Unify the three public pages (à propos, charte, licence) onto a single
grid layout (.page-content) with sticky TOC sidebar, replacing the old
separate / / markup.
- Merge about.php, charte.php, licence.php templates into shared
.page-content / .content-section structure
- Add CommonMark HeadingPermalinkExtension for stable heading anchors
- Use SlugNormalizer for TOC links so they match rendered heading IDs
- Standardize link styling across content blocks: bold black, accent on
hover (consistent with global link style)
- Fix code block wrapping: use pre-wrap instead of pre, constrain grid
columns with min-width:0, auto scrollbar
- Fix apropos page grid placement: force content-section into column 2
so contacts and credits stay in the content area, not the sidebar
Also includes accumulated WIP changes:
- Header gradient: hardcoded purple-to-green (replaces CSS variables)
- Search placeholder font
- Duration field: replace minutes/sec/heures with h:m:s time inputs
- TFE file optional for formats 1,4,6 with client-side JS toggle
- Licence form: em-dash to hyphen, details/summary classes
- Pill search: block Enter key form submission when no results
- Draft autosave: remove CSRF rotation (broke concurrent FilePond uploads)
- Language pill: clear hints for excluded main languages
- Search results: gradient placeholder cards for items without covers
- TFE display: format durée values as XhYm instead of decimal
The closure returned arrays when formData values were arrays (e.g.
jury_promoteur), but the PHP return type annotation was :string.
PHP 8.x enforces this strictly, causing a fatal TypeError in
jury-fieldset.php on add mode.
Also introduces $extraCssAdmin support in head.php for admin-only
stylesheets (form-admin.css, filepond CSS, system.css). Admin pages
now use $extraCssAdmin for admin-only assets and $extraCss for
shared stylesheets like form-base.css.
- New fragment endpoint POST/GET /partage/fragments/draft.php:
saves all form fields to PHP session, excludes file/csrf/slug fields
GET returns JSON for JS hydration on page load
rotates both global CSRF and share CSRF tokens in sync
- form.php accepts optional $formExtraAttrs and $showAutosaveStatus:
allows injecting HTMX attributes and 'Brouillon enregistré' indicator
- renderShareLinkForm adds hx-post with change/input debounce trigger,
loads autosave-handler.js, hydrate fields from draft on page load
- Draft cleared on successful form submission in handleShareLinkSubmission
- autosave-handler.js now also updates share_link_token hidden input
when rotating CSRF token (partage form uses both csrf_token and share_link_token)
- Added .autosave-status CSS to form.css (was admin.css-only)
- Updated fragment routing to accept GET requests (needed for draft hydration)
The partage/admin form had a hardcoded filepond_mode=1 hidden input,
so without JavaScript the server always entered the FilePond async
path — which found no hex IDs and silently dropped all files.
Three-layer fix:
1. HTML: filepond_mode input starts disabled with value=0; JS enables
it and sets value=1 on DOMContentLoaded (and after HTMX swaps).
Disabled inputs aren't submitted → server gets no filepond_mode
→ naturally falls to legacy path.
2. JS: enableFilepondMode() called on page load and hx:afterSwap so
FilePond-enhanced forms always send filepond_mode=1.
3. Server (defense-in-depth): ThesisFileHandler::hasFilePondQueueData()
scans POST['queue_file'] for 32-char hex IDs; ThesisCreateController
and ThesisEditController use it alongside filepond_mode, so even if
the flag somehow arrives without async upload IDs, the path
takes over.
ro=['fire','_read','_write'] is an exclusion list in Ee(), not an inclusion
list. The external pond object has none of these. The only safe interception
point is inside the closure (vendor patch), but the root-cause fix
(fileValidateSizeFilter .filename → .name) already prevents the crash.
Fixes three root causes of FilePond errors on TFE upload forms:
1. server.process.onerror accessed .status on a string (XHR response
text body) — now extracts the body safely.
2. server.load was a bare URL string with no error handling — converted
to object with onload/onerror to prevent FilePond internal _write
crash when load.php returns HTTP errors.
3. destroyFilePondsIn now aborts in-flight processing before pond.destroy()
to prevent stale XHR callbacks firing on a torn-down FilePond instance.
Server-side: FilepondHandler now emits Content-Type: text/plain on all
responses (PHP defaults to text/html on die(), confusing FilePond's
response parser).
Replace text labels (h1, bold, italic) with rendered HTML in the Rendu column:
headings, strong, em, del, code, links, blockquote, lists, hr, sup, small
1. maxFileSize bug: FileValidateSize plugin overrides core's maxFileSize
setter. Core uses toBytes('1GB') = 1073741824, but plugin registers
maxFileSize as [null, Type.INT] which calls toInt('1GB') = 1.
Fix: all maxFileSize and perExtensionMaxSize values as raw bytes.
Also fix option name: fileValidateSizeFilterItem → fileValidateSizeFilter.
2. Temp file persistence: files uploaded via FilePond went to
tmp/filepond/ and vanished from the UI on page reload because
data-existing-files only included DB-persisted files.
Fix: session-track temp file_ids in handleProcess, inject via
getSessionTempFiles() into data-existing-files, teach handleLoad
to stream temp files from disk, and route JS remove → revert for hex IDs.
- Auto-save: new autosave.js with 1.5s debounce, watches all forms with
data-autosave, POSTs to form action with Accept: application/json, shows
saving/saved/error status indicator
- All action handlers (page.php, apropos.php, form-help.php) now detect
JSON Accept header and return {success, csrf_token} or {error} responses
- OverType toolbar enabled (toolbar:true) on all three markdown editors
(page, about_page, form_help)
- Sidebar links: replaced fixed erg_site_url / source_code_url rows with
dynamic sidebar_links array of {label, url} objects. Add/remove via JS.
Fallback migration reads legacy keys if sidebar_links is empty.
- Updated AboutController and about.php template to render dynamic links
- Updated apropos.css: unified .apropos-toc-link replacing .apropos-toc-erg
and .apropos-toc-source
- New CSS: autosave-status states, sidebar-link-row layout
- Removed all Enregistrer + Annuler buttons — auto-save and h1 back-arrow
make them redundant
- Identifiant: mise à jour automatique quand l'année change en back-office (updateThesis + ThesisEditController)
- Contact: hint enrichi (1 seul contact, formatage Instagram/Mastodon)
- Fichiers: TFE rendu optionnel pour Site web/Performance/Installation (note d'intention reste obligatoire)
- Add csv_import queue type (storeAsFile, no async upload) for CSV import dialog
- Convert file-field.php partial to FilePond with field-name→queue-type mapping
- Conditionally skip server config for storeAsFile queues in buildFilePondOptions
- Skip FilePond init for inputs inside closed <dialog> elements
- Trigger FilePond init when import dialog opens
- Load FilePond CSS/JS assets on admin index page
- After relink, always close the modal (even if FilePond input not found,
e.g. page refreshed by live-reload during the fetch).
- After closing, re-fetch #format-fichiers-block via HTMX from
/admin/fragments/fichiers.php?_thesis_id=N which loads thesis files
from DB and re-renders the fragment with pre-populated FilePond pools.
The afterSwap handler auto-reinitializes FilePond instances.
- Updated admin/fragments/fichiers.php to accept _thesis_id, load
existing files from DB, build per-queue-type JSON, and render in
edit mode.
- Fix addFile argument format: FilePond.addFile() takes (source, options)
as two separate arguments, not a single {source, options} object.
- Change .filepond--file default border from accent-yellow to accent-green.
Existing files loaded in edit mode have type 'local' and never reach
processing-complete state, so they got the yellow border.
- Change relinked file add from type 'local' to 'limbo'. Limbo items
go through DID_COMPLETE_ITEM_PROCESSING which triggers onprocessfile
(ensures syncOrderInput runs with serverId available) and renders
the green checkmark visual.
- Await addFile Promise and close modal in .then() instead of
immediately, ensuring the item is created before cleanup.
- Remove duplicate modal.close() after the addFile block.
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
Extract shared filepond logic into src/FilepondHandler.php class.
Admin filepond endpoints delegate to the handler after AdminAuth check.
New partage filepond endpoints at /partage/actions/filepond/ verify
share_active session flag + CSRF token, no admin auth required.
JS reads filepond-base meta tag to determine endpoint path:
- Admin pages: /admin/actions/filepond (via head.php isAdmin check)
- Partage form: /partage/actions/filepond (explicit meta)
partage/index.php sets share_active = true on form render, cleans up on
successful submit. Partage process endpoint rate-limited to 30/5min per
session. No nginx changes needed — /partage/ location already handles
PHP without auth_basic.
- Remove separate video/audio/peertube_video/peertube_audio pools from UI
- TFE pool now accepts all file types including video/audio
- When PeerTube is enabled, video/audio dropped into TFE pool auto-upload
to PeerTube (process.php detects MIME and uploads immediately)
- PeerTube return IDs now encode type: peertube:video:UUID or peertube:audio:UUID
- load.php returns placeholder SVG for PeerTube files so they appear in FilePond
- Edit mode: all existing files (including PeerTube) shown in TFE FilePond pool
- Remove legacy video/audio/peertube_* handling from both controllers
- Remove unused vide/audio/peertube_* entries from JS QUEUE_CONFIG
- Delete file-upload-queue.js (495 lines of custom queue logic)
- Delete sortable.min.js dependency
- Add file-upload-filepond.js: thin wrapper that upgrades .tfe-file-picker
inputs to FilePond instances with storeAsFile:true for native multipart
form submission (no form-submit interception needed)
- Update fichiers-fragment.php: replace queue container <ul> elements
and empty-state <p> with bare <input> elements that FilePond upgrades;
change name attributes to queue_file[tfe][] etc. for PHP compatibility
- Update add.php, edit.php, partage/index.php: swap JS/CSS refs
- Clean up form.css: remove .fq-* and .tfe-file-queue custom styles,
add FilePond theme overrides matching xamxam design tokens
- Update dead-code fieldset-files.php for consistency
Server-side stays unchanged: PHP receives ['queue_file']['tfe'][]
exactly as before through native multipart submission.
- Replace fetch(redirect:manual) with XMLHttpRequest in file-upload-queue.js.
The previous fetch-based redirect detection was broken because opaque
redirects hide the Location header. XHR's responseURL reliably exposes
the final URL after server-side redirects.
- Add console.log tracing at every decision point in submit interception:
entry, hasFiles check, enctype check, double-submit guard, XHR status,
redirect detection, error fallback.
- Add error_log entry-point logging to all 16 admin action files plus
the partage/index.php submission handler and password gate. Each logs:
request method, content type/length, POST keys, file counts, and
queue-specific file counts where applicable.
- Add double-submit guard (_xamxamActiveSubmit) to prevent duplicate
XHR sends when the native submit handler fires after interception.
Drops the session-backed HTMX incremental upload system in favour of a
single JS module that manages `File` objects client-side and injects
them into `FormData` on submit.
Key changes:
* `file-upload-queue.js`: client-side queues with validation, reorder
(SortableJS), removal, dirty-state tracking, and fetch-based submit
with manual redirect handling
* `fichiers-fragment.php`: empty queue containers for JS-managed queues;
HTMX format switching still works with queue rehydration after swap;
annexe uploads now support multiple files
* Form UI cleanup: moved existing files and cover preview into the
`Fichiers` fieldset (edit mode); removed redundant queue labels while
keeping labels for single-file inputs (`couverture`,
`note d'intention`); added delete buttons for existing files
* `ThesisFileHandler.php`: added
`handleTfeQueueFiles()`/`handleAnnexeQueueFiles()` reading from
`$_FILES['queue_file']`; introduced `extractFilesSubArray()` for
nested upload arrays; removed session-based queue handling
* `ThesisCreateController.php` &
`ThesisEditController.php`: switched to extracted
`['queue_file']` uploads
* `beforeunload-guard.js`: now also watches
`window.__xamxamDirty`
* Deleted obsolete PHP upload/remove/reorder queue endpoints for
`partage` and `admin`
* Cleaned up route dispatch in `partage/index.php`
* Misc form and styling updates in templates/CSS
* Added `docs/cms-migration-plan.html`
Replace the client-side FileArray + Sortable drag-to-reorder with a
server-side session-based upload flow:
- New endpoints: /partage/upload-tfe-file, /partage/remove-tfe-file
(and /admin/ variants) — single-file incremental upload via HTMX
multipart/form-data with progress bar support
- Session storage: uploaded files go to STORAGE_ROOT/uploads/{session_id}/
with metadata in $_SESSION['tfe_uploads']
- file-upload-queue.js reduced to single-file previews only (couverture,
note_intention, annexes thumbnails)
- ThesisFileHandler gains handleTfeFilesFromSession + writeTfeFileFromSrc
+ cleanupSessionUploads for final commit from session temp
- Sortable.min.js removed from all script tags; drag handles and ghost
CSS removed
- No file_orders[]/file_labels[] hidden field injection needed
- Upload queue survives page refresh (server-owned list)
This eliminates the SortableJS dependency entirely while keeping the
same UX: pick files, see them in a queue, remove individual files.
- Replace mb_strlen/mb_substr/mb_strtolower with strlen/substr/strtolower
(mbstring extension missing on server, causing fatal error)
- Scope annexes checkbox HTMX swap to #annexes-input-block with hx-select
(prevents duplicating entire page inside Fichiers fieldset)
- Split format+fichiers response: #format-fichiers-block (stable) and
#format-extras-block (swappable, inside Fichiers fieldset). Format
checkboxes use hx-select to extract only the extras, preserving file queue.
- Keep format extras inline in Fichiers fieldset (no sub-fieldsets). Remove
website legend input (URL only).
- When PeerTube upload disabled, show direct file upload inputs for
video/audio (name=files[]).
- Add "Glissez-déposez" sort hint below TFE file queue.
- Fix .fq-name overflow with width:0;min-width:100% chain.
- Remove legend placeholder from .fq-item.
- Merge "Récits et expérimentation" AP into "Narration Spéculative".
Rename PACS to "Pratique de lart - outils critiques, arts et contexte
simultanés".
- Remove président·e field from jury fieldset, form templates, and
controller validation. Keep DB column and display logic for existing data.
- Edit mode now uses the same fichiers-fragment.php as add and partage,
instead of duplicating the format checkboxes + new-file upload + website
URL fieldsets.
- Edit-only elements (existing files list, cover replace) stay in
a separate #edit-existing-files-block below the shared fragment.
- Removed .zip/.tar/.gz from the main TFE upload accept in both
fichiers-fragment.php and fieldset-files.php. Archives go only
in the Annexes file input.
- Removed admin/format-website-fragment.php dependency from edit
(no longer needed — the shared fragment handles website too).
fix: jury repop crash + hx-preserve on file inputs, remove zip/tar from tfe accept
- Jury fieldset add-mode repopulation now handles both scalar (legacy)
and array (new dynamic multi-row) values for jury_promoteur and
jury_promoteur_ulb_name. htmlspecialchars() was choking on array value.
- All file inputs in fichiers-fragment.php wrapped in hx-preserve
containers so HTMX swaps don't wipe user-selected files when toggling
formats or the annexes checkbox.
- Removed .zip/.tar/.gz from main TFE file accept — archives only via
annexes input (which already had multiple + correct accept).
- Edit mode now reuses the same fichiers-fragment.php fragment.
fix: file inputs re-initialize after HTMX swap via inline script
- Exposed window.XamxamInitFileUploads from file-upload-queue.js IIFE
so HTMX fragments can trigger re-binding without a global listener.
- fichiers-fragment.php emits <script>XamxamInitFileUploads()</script>
at the end of the #format-fichiers-block fragment.
- Removed hx-preserve wrappers — they prevented re-render after
format/annexes toggles changed visible inputs.
- This also fixes .zip removal from TFE accept and jury repopulation
array crash from the previous commit.
refactor: simplify file-upload-queue.js, remove file-preview.js
- file-upload-queue.js rewritten from ~250 lines to ~120 lines:
no more DataTransfer machinery, no IIFE wrapper, uses .onchange
instead of addEventListener for simpler HTMX re-init.
- window.XamxamInitFileUploads is the function itself (not an IIFE export).
- Merged file-preview.js functionality into file-upload-queue.js
(single-file .data-preview handling). Deleted file-preview.js.
- fichiers-fragment.php inline script calls XamxamInitFileUploads()
after every HTMX swap (same as before).
debug: add console.log to file-upload-queue.js for file input behavior
Adds logging at key points to diagnose why only one file is displayed:
- XamxamInitFileUploads called
- TFE queue picker init (id, multiple attribute state)
- onchange event (files count, names)
- fileArray post-concat length
- Single-file preview bindings (id, multiple attribute)
Remove after debug session.
Created templates/partials/form/form.php as the unified form template driven by
$mode ('add'|'edit'|'partage') and boolean flags for optional sections.
The three calling templates (templates/admin/add.php, templates/admin/edit.php,
partage/index.php renderShareLinkForm) now only set variables then include the
shared partial. ~200 lines of duplicated fieldset HTML eliminated.
- Removed the `vimeo/psalm` dependency and all related files
(`psalm.xml`, `psalm‑baseline.xml`, suppress annotations).
- Added **PHPStan** (v2.1.54) and **PHP‑CS‑Fixer** (v3.95.1) to
`vendor/bin/`.
- Created `phpstan.neon` (level 5, bootstraps `app/bootstrap.php`,
scans `Parsedown.php`).
- Created `phpstan‑baseline.neon` with 10 pre‑existing errors.
- Added `.php‑cs‑fixer.dist.php` (PSR‑12 + PHP80Migration, targets
`app/src` & `app/tests`).
- Added `biome.json` and updated `justfile` to replace the old Psalm
recipes with `phpstan`, `cs‑check`, and `cs‑fix`.
- Updated `.gitignore` to exclude PHPStan and PHP‑CS‑Fixer cache files.
- Updated several JS files (`file‑preview.js`, `file‑upload‑queue.js`)
eand PHP controllers (`MediaController.php`, `SearchController.php`,
`SystemController.php`).
- Minor adjustments to `TODO.md`, `app/src/Database.php`,
`app/src/Parsedown.php`, `app/src/ShareLink.php`, and
`app/src/SmtpRelay.php`.
- Live file preview on all file inputs (file-field partial, edit template):
thumbnails for images, emoji icons for PDF/video/zip/vtt, filename + size
- New file-preview.js wired via $extraJs in add.php / edit.php and direct
<script> in partage/index.php; $extraJs support added to head.php
- admin/recapitulatif.php: replace plain table with rich file list — image
thumbnails linked to media.php, type badges, human-readable size, date
- partage/recapitulatif.php: full rewrite — shows thesis metadata + files
list with same rich display (no media links for student privacy)
- form.css: new sections for .file-preview-list (live preview) and
.recap-file-list / .recap-dl / .partage-recap (recap pages)