diff --git a/TODO.md b/TODO.md
index 9457f62..96c123f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -7,7 +7,6 @@
## Pending
- [ ] #test-env-deploy-code-chown-regression test-env deploy-code --chown regression documented in README --context test-env
-- [ ] #decide-fate-of-stray [!low] Decide fate of stray tracked commits.txt (6268-line git log dump) — Fate: left tracked; out of scope for history cleanup
- [ ] #history-cleanup-66-commits [!medium] History cleanup: 66 commits squashed to 29 on top of main; pre-squash backup at refs/backup/pre-squash-tip (tree 8649514e) + git bundle /tmp/xamxam-presquash.bundle
## Completed
@@ -71,6 +70,7 @@
- [x] #backup-branch-from-current [!high] Backup branch from current state (backup/pre-history-cleanup)
- [x] #squash-main-history-into [!high] Squash main..@ history into logical commits — Groups: (1) session-timeout+GC; (2) env/provision+docs; (3) coverage/accents; (4) cover-images; (5) admin-cleanup-page; (6) test-env+deploy; (7) admin-index-css; (8) toc; (9) misc features; (10) migrations/justfile/biome. Fold TODO-only commits. Keep postlaunch intact.
- [x] #verify-final-tree-identical [!medium] Verify final tree identical to backup
+- [x] #decide-fate-of-stray [!low] Remove stray tracked commits.txt (6268-line git log dump) — Removed; no code references it
## Deferred / Blocked
- [ ] #just-setup-backs-a [!medium] just setup backs a stale setup-dev.sh (clones php-live-reload, legacy admin/data/ dirs) — needs rewrite or removal
diff --git a/commits.txt b/commits.txt
deleted file mode 100644
index a3c07ea..0000000
--- a/commits.txt
+++ /dev/null
@@ -1,6268 +0,0 @@
-72d3075466cc | 2026-06-29 17:27
-
----
-a790ddc02ad0 | 2026-06-24 14:57
-Fix admin login heading: prevent 'Administration' from hyphenating or overflowing box
-
----
-21f16ee093ec | 2026-06-24 14:49
-chore: move #rep-polish to Completed, investigate #tighten-csp blockers
-
----
-eb706214ce16 | 2026-06-24 14:45
-feat: scroll-position memory on repertoire HTMX swaps + swap transition polish
-
-- Add repertoire-scroll-restore.js: snapshots scrollTop of each column
- before htmx:beforeSwap, restores after htmx:afterSwap (keyed by data-col)
-- Add subtle opacity transition on #repertoire-index during htmx-swapping
-- Tighten rep-indicator opacity transition to 0.1s for snappier feedback
-- Import new module in public-entry.js
-
----
-e0cf9f8f5746 | 2026-06-24 14:26
-chore: update TODO — mark icon-color-verify and 3 security tasks complete
-
----
-0062b296782e | 2026-06-24 14:17
-Fix security issues from audit: gate partage fragments on share_active session, add CSRF to retry-email POST, remove dead App::verifyCsrf()
-
----
-84869ad96811 | 2026-06-24 14:15
-Fix open redirect in tag.php + language.php: reject protocol-relative URLs (//evil.com) by also checking for // prefix
-
----
-6ecd3d4540ef | 2026-06-24 13:21
-Fix biome lint errors: remove duplicate CSS properties, apply safe auto-fixes
-
-CSS:
-- Remove duplicate 'background' fallbacks in base.css, header.css, search.css
- (solid color declared before gradient — gradient always wins)
-- Remove duplicate 'padding' in admin.css .admin-import-log
-
-JS (biome --write safe fixes applied):
-- function() → arrow functions in all IIFEs and callbacks
-- forEach/callback → arrow functions
-- evaluePtrn → parseInt(x, 10) in admin-contacts-form.js
-- Cleaned label text in build.mjs lint step
-
-Remaining warnings are intentional: !important overrides, descending
-specificity (admin.css cascade), noUnusedVariables (functions exported
-to window/onclick), useTemplate style preference.
-
----
-82d3dcb084b7 | 2026-06-24 13:18
-Fix repertoire column scrolling + admin TOC duplication
-
-- repertoire.css: add min-height: 0 to column
scroll containers so
- grid 1fr row shrinks below content and overflow-y: auto activates
-- admin-toc.js: add __adminTocBuilt guard + nav.children check to prevent
- double population when loaded both via admin.min.js and direct
- 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.
-
----
-8f4f9d00b457 | 2026-05-08 17:03
-Refactor: Form improvements and cleanup: note contextuel, annexes, fichiers
-
-1. fix: form improvements — multiple promoteurices, asterisks, contact dedup, bentopdf
-
-- Multiple promoteurice (interne + ULB): both fieldsets now support dynamic
- add/remove rows (same pattern as lecteurs). field names changed to arrays
- (jury_promoteur[], jury_promoteur_ulb_name[]). Controllers accept both
- scalar and array forms for backwards compat.
-- ULB promoteurice: when finality=Approfondi, asterisk appears on legend
- and first ULB input is marked required (JS toggle). Non-Approfondi hides
- the fieldset and clears values.
-- Contact visibility duplication: removed redundant contact_public checkbox
- from admin add/edit forms (showContact=false). The 'mail' field in
- fieldset-tfe-info already serves this purpose.
-- Asterisk fixes: website URL field now has asterisk+required when Site web
- format selected. Video/audio already had correct required handling.
-- bentopdf link: clearer full URL 'https://bentopdf.com/' in both
- fichiers-fragment.php and form.php (edit mode)
-
-2. refactor: merge Note contextuelle into Backoffice, add Lien BAIU, reorder fields
-
-Backoffice fieldset now contains in order:
-1. Note contextuelle (was standalone fieldset)
-2. Points du jury
-3. Remarques
-4. Lien BAIU (moved from Métadonnées complémentaires)
-5. Exemplaire physique BAIU
-6. Exemplaire physique ERG
-7. Contact interne
-
-
-Métadonnées complémentaires now only has: pages, minutes, annexes checkbox.
-Removed dead showContextNote variable from form.php, add.php, edit.php.
-Controller baiu_link still mapped to input name "lien" (no migration needed).
-
-3. refactor: move annexes checkbox from Métadonnées into Fichiers fieldset
-
-- Removed 'Ce TFE comporte des annexes' checkbox from
- fieldset-metadata.php.
-- Added annexes checkbox + conditional file input to
- fichiers-fragment.php. When checked, an HTMX swap reveals
- the 'annexes' file input (multiple, PDF or ZIP/TAR, max 500 MB).
-- form.php seeds ['has_annexes'] for initial fragment render.
-- Métadonnées complémentaires now only contains pages + minutes.
-
----
-03c5fd217eaa | 2026-05-08 16:48
-feat: dual upload system — direct file storage + PeerTube API integration
-
-Adds a parallel PeerTube upload system behind a feature flag (disabled by default
-until upload quota is granted). When disabled, the existing direct file upload
-path works unchanged.
-
-Files:
-- src/PeerTubeService.php — credential storage (encrypted), OAuth2 token
- retrieval, multipart upload to /api/v1/videos/upload
-- migrations/021_peertube_settings.sql — peertube_settings singleton table
- + peertube_upload_enabled site_setting (default 0)
-- admin/actions/settings.php — peertube section handler
-- admin/parametres.php / templates/admin/parametres.php — PeerTube UI section
-- partage/fichiers-fragment.php — shows file inputs when enabled, TODO notice otherwise
-- ThesisCreateController / ThesisEditController — handlePeerTubeUpload()
-- tfe.php — PeerTube iframe embed detection
-- AdminLogger — logPeerTubeUpdate()
-
----
-11e61226e2aa | 2026-05-08 16:35
-fix: justfile shebang recipes indentation (spaces → tabs)
-
----
-e6829994b6d1 | 2026-05-08 13:03
-Refactor + feat: unify format/fichiers HTMX fragment, reorder format types, add file constraints, fix admin auth
-
-* **Unified Format + Fichiers into a single HTMX fragment**
-
- * Introduced `app/public/partage/fichiers-fragment.php` as shared dynamic block returning both format checkboxes and adaptive “Fichiers” fieldset
- * Logic adapts inputs based on selected formats:
-
- * no selection / upload formats → standard file inputs
- * “Site web” → URL fields only
- * “Site web + upload” → file inputs + URL sub-fieldset
- * Added admin wrapper: `app/public/admin/fichiers-fragment.php` (gated via `admin_mode=1`)
- * Added `app/public/admin/format-website-fragment.php` for edit-mode website URL toggling
- * Wired route `/partage/fichiers-fragment` in `app/public/partage/index.php`
- * Refactored `form.php` (add/edit partage) to use single `#format-fichiers-block` instead of separate fragments
- * Edit mode format checkboxes now target `format-website-fragment.php` → `#edit-website-url-fieldset`
- * Added `$hxInclude` support in `checkbox-list.php` for configurable HTMX includes
-
-* **Format system migration + ordering**
-
- * Migration `020_format_types_sort_and_rename.sql`:
-
- * added `sort_order` column to `format_types`
- * inserted new format **Image**
- * defined ordering: Écriture · Image · Audio · Vidéo · Site web · Performance · Objet éditorial · Installation · Autre
- * `Database.php`: format queries now use `ORDER BY sort_order, id`
- * `fichiers-fragment.php`:
-
- * uses ordered format list
- * resolves Image/Vidéo/Audio by name
- * introduces `$hasImage` flag
- * preserves `admin_mode` across HTMX requests
-
-* **File constraints and UX updates**
-
- * Enforced **100 MB PDF limit**
-
- * `ThesisCreateController`: `MAX_PDF_SIZE = 100MB` for PDFs only
- * `ThesisEditController`: same PDF-specific constraint applied
- * Other file types remain capped at 500 MB
- * Updated UI hints in `fichiers-fragment.php` and edit form:
-
- * explicitly mention 100 MB PDF limit
- * added reference to `bentopdf.com` for compression guidance
- * `file-field.php`: added `$hintRaw` to allow HTML rendering in hints
-
-* **Admin authentication fix**
-
- * Fixed missing auth in admin fragments
- * Added `require_once AdminAuth.php`
- * Replaced direct usage with `AdminAuth::requireLogin()`
- * Applied consistent pattern with existing fragment authentication approach
-
-* **Migrations included**
-
- * `019_add_ecriture_format.sql`
- * `020_format_types_sort_and_rename.sql`
-
-* **Files affected**
-
- * Controllers: `ThesisCreateController`, `ThesisEditController`
- * DB layer: `Database.php`
- * Public fragments: `partage/fichiers-fragment.php`, `admin/fichiers-fragment.php`, `admin/format-website-fragment.php`
- * Templates: `form.php`, `checkbox-list.php`, `file-field.php`
- * Routing: `partage/index.php`
- * Misc: `TODO.md`
-
-This consolidates format normalization, HTMX UI simplification, file validation rules, and admin stability fixes into a single coherent system update.
-
----
-7e35bba5304c | 2026-05-08 12:48
-Encrypt SMTP password at rest with AES-256-GCM
-
----
-95fcbc919a44 | 2026-05-08 12:40
-Remove required from all admin add/edit form inputs
-
-- Skip required-field validation for orientation/ap/finality/licence/jury in admin add+edit
-
----
-5735ccbc3878 | 2026-05-08 11:30
-Fix issues with nginx access to pages
-
-- fix: 403 on /language-autre-fragment.php — add explicit nginx location block
-
- The nginx catch-all blocked direct access
- to all PHP files except /index.php and files inside /admin/.
-
- language-autre-fragment.php lives at the public root and is POSTed to by
- HTMX from both the admin edit form and the partage form. Added an explicit
- fastcgi block so it is executed
- rather than denied.
-
-- fix: replace .php-suffixed public URLs blocked by nginx catch-all
-
- Audit of all client-facing PHP URL references against nginx routing:
-
- - fetch('/request-access.php') in tfe.php -> '/request-access'
- (clean URL already routed by Dispatcher)
- - /media.php?path= in form.php (x2) and admin/recapitulatif.php -> /media?path=
- (nginx only has location = /media, no location for /media.php)
-
- All these .php-suffixed URLs hit the nginx catch-all
- location ~ \.php$ { deny all; }
- which takes precedence over location / { try_files ... } for regex matches.
-
----
-6ba13e00eabd | 2026-05-08 10:56
-test: add ShareLinkTest + PureLogicTest (TDD), fix coverMap undefined in SearchController
-
----
-15d54fa19ee3 | 2026-05-08 10:55
-add Néerlandais language option and make language_autre conditionally required
-
----
-f3d961556260 | 2026-05-08 10:46
-merge banners into covers: remove banner field, migrate files, add covers to search/home/repertoire cards
-
----
-e3896811c416 | 2026-05-07 23:45
-Fix migrations and deploy issues + errors + linting
-- scan both pending/ and applied/ dirs so remote catch-up works
-- fix remote 500s: run.php handles per-statement errors so VIEW rebuilds run after duplicate columns; replace mb_strimwidth with substr (no mbstring extension on server)
-- add missing migration: 015_license_custom.sql (column existed in schema.sql but was never migrated)
-- remote: fgetcsv enclosure single-char + AdminLogger permission-denied
-guard + deploy always migrates
-- fix admin-filters wrapping: restore flex-wrap, flex-basis on
-inputs/selects, shrink-protect buttons
-- fix phpstan: remove redundant ?? [] after isset guard in
-ThesisEditController
-- biome: exclude vendored min.js via includes patterns;
-lint whole js dir; modernise beforeunload-guard.js
-
----
-bdd95341b09c | 2026-05-07 22:48
-Extract shared TFE form partial — single source of truth for add/edit/partage
-
-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.
-
----
-ac0008df6cc5 | 2026-05-07 21:24
-Add website-type TFE support: URLs stored as thesis_files rows, HTMX-toggle on Site web format
-
----
-9dc7ea98f252 | 2026-05-07 20:58
-fix: password-protected share links never load form after password entry
-
-The main GET handler in partage/index.php always showed the password gate
-for links with password_hash set, even after successful verification. The
-session flag share_verified_ was being set by requirePasswordGate()
-but never checked when deciding whether to re-show the gate.
-
-Added a check: if the session flag is already set, skip the gate and
-render the form directly.
-
-Also added error_log() calls throughout the password flow to help
-diagnose future issues.
-
----
-03121d6b7ef5 | 2026-05-07 20:52
-form: add spacing between elements inside fieldsets
-
-- common.css: fieldset > *:not(:last-child) gets margin-bottom: var(--space-xs)
-- admin.css: .param-form fieldset > * zeroes margin to avoid double-spacing with flex gap
-
----
-d9dd4bdbc777 | 2026-05-07 20:42
-Edit Email confirmation styling
-
----
-696259afae08 | 2026-05-07 19:54
-Fix form field required states & missing fields per spec
-
-- Admin add: add contact_public checkbox (matching edit form)
-- All forms: formats checkbox-list now required
-- All forms: jury promoteur·ice interne required, lecteur·ice interne/externe required
-- All forms: licence select now required
-- Admin edit: add E-mail de confirmation fieldset
-- Partage: contact always visible when provided (no contact_public field)
-- Partage: filter PACS from AP programs dropdown
-- Server-side validation: formats, jury, licence required (create + edit controllers)
-- Autofocus mappings for new validation errors
-- No duplicate asterisks — verified across all rendered fields
-- fix: add missing old() function in admin edit controller
-- refactor: move admin email field to Backoffice as Contact interne, never send email
-- Untrack admin.log (covered by .gitignore)
-
----
-51f9f56e0995 | 2026-05-07 19:40
-Replace span with a link + href in about.php for credits
-
----
-e0c748d8e7b0 | 2026-05-07 18:44
-Refactor about.php
-
-- Hardcode source code URL and credits in about template, remove from DB/admin interface; only contacts remains editable
-- Merge apropos editables into one À propos section, remove charte, add editable source code URL
-
----
-24d68dda598e | 2026-05-07 17:52
-refactor form structure per new spec + fix
-- split jury into interne/externe/ULB,
-- remove president from student form,
-- add language_autre,
-- split duration into pages+minutes+annexes,
-- move licence to degrés d'ouverture with CC2r,
-- add license_custom,
-- filter PACS from student AP list,
-- editable généralités help block,
-- Libre toggle per settings
-
-Fix:
-- missing comma after cc4r column in schema.sql
-- remove duplicate form footer from partage template
-- remove couverture from student files fieldset; add promoteur ULB conditional disable via JS on Approfondi
-- promoteur ULB: remove 'si applicable', make required when visible
-
----
-dce0e0b30194 | 2026-05-07 17:30
-schema: validate against new TFE field spec
-- add exemplaire_baiu, exemplaire_erg, cc4r, remarks;
-- add is_ulb to jury;
-- split jury_lecteurs into interne/externe in view;
-- refactor admin edit form with backoffice fields;
-- update public fiche to show promoteur ULB and split lecteurs
-
----
-7793b6f86d4f | 2026-05-07 16:28
-add file export system for admins
-
-- ExportController: getAllThesisFiles(), buildExportManifest(), createExportZip()
- builds a ZIP archive with manifest.json + files/ mirror of storage/theses/
-- Database: getAllThesisFilesForExport() queries all thesis_files + identifier
-- AdminLogger: logFilesExport() audit log entry
-- admin/actions/export-files.php: thin dispatcher, streams zip with headers
-- templates/admin/index.php: 'Exporter fichiers' button next to CSV export
-
----
-821369f00468 | 2026-05-07 12:27
-exclude maintenance.flag from rsync deploy and git
-
----
-3f87d71e389e | 2026-05-05 18:54
-Fix: CSV importer and imported data
-- pad rows, distinguish empty year, better error diagnostics
-- derive year from identifier when year column is empty
-- fix remaining 18 theses: Installation/Performance (slash→dash) orientation alias
-- csv importer: use column-name-based header detection instead of hardcoded positions
-
----
-b063312642c5 | 2026-05-05 18:27
-centralise repertoire filter column rendering
-- shared repFilterEntry() and config array
-- shared repFilterEntry() and $filterColumns config array
-- fix single-valued FK fading via full intersection
-
----
-bca707ee9687 | 2026-05-05 11:34
-standardise buttons: .btn base class (border-radius 10px, padding var(--space-xs))
-
----
-b58445f71c84 | 2026-05-05 11:09
-sticky save/cancel buttons at top-right of admin edit page
-
----
-95066de7b4d3 | 2026-05-05 10:31
-standardise multi-author support across all forms
-
-- ThesisCreateController: comma-split auteurice, sort alphabetically,
- use setThesisAuthors() instead of hardcoded createThesis() author_id
-- Database::createThesis(): removed author_id param and hardcoded insert
-- Database::findDuplicateThesis(): accepts array of author names, matches
- any shared author via IN + DISTINCT
-- ThesisEditController::save(): sort authors alphabetically on save
-- File folder naming: slug from all authors alphabetically sorted
-- v_theses_full GROUP_CONCAT: ORDER BY a.name ASC for deterministic display
-- Migration 012_author_view_order.sql: rebuilds view with alphabetical order
-
----
-125c501f4080 | 2026-05-04 18:33
-Fix 403 on HTMX fragment requests: AdminAuth Basic Auth sets session key
-
----
-37111eaac452 | 2026-05-04 18:19
-fix: add missing remote DB migrations and deploy-migrate recipe
-
-Four ALTER TABLE / CREATE TABLE statements were applied locally but never
-deployed to the remote production database, causing:
-
-- acces.php → 500: share_links.is_archived missing (ShareLink::listActive/listArchived)
-- parametres.php → 500: smtp_settings.notify_email missing (SmtpRelay::getSettings)
-- /tfe?id=N → redirect-to-home: thesis_files.sort_order missing (getThesisFiles ORDER BY)
-- admin_audit_log table missing (AdminLogger::insertDb, best-effort but noisy)
-
-Adds four pending migrations (008–011) covering all missing schema changes.
-Adds 'deploy-migrate' just recipe to run migrations on the remote after deploy.
-
----
-ae6d9b86b32a | 2026-05-04 17:52
-Replace browser alert/confirm dialogs with