diff --git a/TODO.md b/TODO.md index 551ea9a..9c09de5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,131 +1,31 @@ # TODO -## HTMX v2 Migration +> Last updated: 2026-06-11 +> Context: Form Accessibility & Resilience improvements for XAMXAM thesis submission platform -Reference: `docs/autosave-system.md` → "HTMX v2 Migration Plan" section. +## In Progress -- [x] `contenus-edit.php` (pages): Add `hx-*` attrs, add `overtype:change` dispatch in OverType `onChange` -- [x] `contenus-edit.php` (form_help): Add `hx-*` attrs, add `overtype:change` dispatch in OverType `onChange` -- [x] `apropos-groups-form.php` (contacts): Add `hx-*` attrs only -- [x] `contenus-edit.php` (sidebar_links): Add `hx-*` attrs only -- [x] Add `handleAutosaveResponse()` shared handler + `htmx:beforeRequest` loading state -- [x] Delete `autosave.js` -- [x] Fix backend `$isAjax` detection: also recognize `HX-Request` header (page.php, apropos.php, form-help.php) -- [x] Form-help inline editors: add OverType toolbar + HTMX auto-save + remove save buttons -- [x] Markdown cheatsheet modal: reusable dialog on all OverType editors +## Pending -## FilePond crash on TFE upload forms +- [ ] #aria-test-manual Test WCAG changes with VoiceOver and NVDA on full add/edit/partage form flows +- [ ] #nojs-upload-test Test end-to-end: submit partage form with JS disabled, verify files arrive via `$_FILES` +- [ ] #filepond-preserve Preserve FilePond temp file IDs on partage validation redirect `(partage/index.php)` `(FilepondHandler.php)` +- [ ] #two-phase-commit Add two-phase commit: INSERT thesis `status='draft'`, COMMIT, move files, UPDATE to `active` `(ThesisCreateController.php)` +- [ ] #cleanup-drafts Add periodic cleanup job for orphaned drafts (`just cleanup-drafts`) -- [x] Analyze root cause → `docs/filepond-crash-analysis.md` -- [x] Partial fixes (Content-Type headers, onerror cleanup, load object) — insufficient, crash still reproduces -- [x] HTMX/destroy race hypothesis investigation → `docs/filepond-race-investigation.md` (verdict: REFUTED) -- [x] Diagnostic probes + deep analysis: confirmed load-file-error dispatch path, traced via error.stack to fileValidateSizeFilter line 389 -- [x] **ROOT CAUSE FIXED**: fileValidateSizeFilter accessed `item.filename` but FileValidateSize's LOAD_FILE filter passes the raw File/Blob (which has `.name`, not `.filename`). Changed to `item.filename || item.name`. Also added null guard to getExt(). -- [x] Defensive: Wt and Fr crash guards in filepond.min.js prevent action.status.main crash -- [x] process.onload: replaced throw with error-marker return (prevents FilePond crash when server returns HTML) -- [x] Routing: partage index.php now routes /partage/actions/* directly to PHP files (was treating 'actions' as a slug and returning full HTML page) -- [x] **All crashes resolved** — verified working on partage form +- [ ] #form-setup-helper Add `ThesisFormSetup` helper class to reduce bootstrap duplication across add/edit/partage `(partage/index.php)` `(admin/add.php)` `(admin/edit.php)` -## Form Accessibility & Resilience — Assessment Follow-up +## Completed -Reference: Assessment against progressive-enhancement / WCAG-AA / "never lose data" / low-common-denominator guidelines. +- [x] #refactor-partage Extract partage form page chrome to `templates/partage/form-page.php` `(partage/index.php)` ✓ +- [x] #htmx-migration HTMX v2 migration: OverType editors, autosave handler, backend `HX-Request` detection ✓ +- [x] #filepond-crash FilePond crash on TFE upload forms: root cause fixed (`.filename` → `.name`), all crashes resolved ✓ +- [x] #aria-errormessage WCAG AA: field-level `aria-errormessage`, `aria-invalid`, `aria-describedby` on all form fields ✓ +- [x] #nojs-upload-fix No-JS file uploads: `filepond_mode` default to `0 disabled`, server-side `$_FILES` fallback ✓ +- [x] #autosave-partage Autosave text fields on partage form: session draft endpoint (`fragments/draft.php`), HTMX autosave on change/input, page-load hydration, "Brouillon enregistré" indicator, draft cleared on submit ✓ +- [x] #mobile-responsive Mobile-responsive form layout: `@media (max-width: 600px)` breakpoint, 44×44px touch targets ✓ +- [x] #aria-fieldset-fix Remove invalid `required` attribute from `
`, keep `aria-required="true"`, add `role="group"` ✓ +- [x] #split-form-css Split `form.css` into `form-base.css` and `form-admin.css` ✓ +- [x] #extra-css-admin Update `head.php` to support `$extraCssAdmin` for admin-only stylesheets `(head.php)` ✓ -### 1. WCAG AA: Add field-level `aria-errormessage` + `aria-invalid` to TFE form - -**Current state:** Flash error divs have `role="alert"` but individual fields are never linked to their error via `aria-errormessage`. The `autofocusFieldForError()` mechanism focuses the field after a validation redirect but does not announce the error to screen readers. Help `` text is not linked via `aria-describedby`. - -**To do:** -- [x] Extend `text-field.php`, `select-field.php`, `checkbox-list.php` partials to accept optional `$errorFieldName` variable -- [x] When `$errorFieldName` matches the field, add `aria-errormessage="flash-error"` and `aria-invalid="true"` on the input -- [x] On validation redirect, populate `$errorFieldName` from `App::consumeAutofocus()` so each failing field references the flash error container -- [x] Add `aria-describedby` linking each `` hint to its input (always, not just on error) -- [x] Give flash-error div `id="flash-error"` and `tabindex="-1"` for programmatic reference -- [x] Wire `App::flashAutofocus()` into partage submit catch block (was missing) -- [x] Wire `$withAutofocusFn` in admin add template (was defaulting to identity) -- [x] Apply `aria-invalid` + `aria-errormessage` on synopsis textarea (not in text-field partial) -- [x] Apply `aria-describedby` on file inputs in `fichiers-fragment.php` -- [x] Apply format checkboxes `aria-invalid` support in `fichiers-fragment.php` -- [ ] Test with VoiceOver and NVDA on the full add/edit/partage form flows - -### 2. No-JS file uploads silently fail (data loss) - -**Current state:** `form.php` hardcodes ``. Without JS, no `queue_file[]` hidden inputs are populated → server gets `filepond_mode=1` with empty queue → all files silently dropped. The form is supposed to work without JS. - -**To do:** -- [x] Change the hidden input to `` by default; JS enables it and sets `value="1"` on DOMContentLoaded -- [x] Add server-side fallback in `ThesisCreateController::submit()` and `ThesisEditController::save()`: when `filepond_mode=1` but no `queue_file` data is present, fall through to the legacy `$_FILES` path -- [ ] Test end-to-end: submit the partage form with JS disabled, verify files arrive via `$_FILES` - -### 3. Autosave text fields on partage form - -**Current state:** Only FilePond files are persisted to the server before submit. Text/select/checkbox fields live only in DOM and are lost on tab close / crash / network failure. `beforeunload-guard.js` warns but cannot recover data. The admin panel already has an HTMX-based autosave pattern (`contenus-edit.php`). - -**To do:** -- [ ] Implement a per-session draft endpoint (`POST /partage//draft`) that saves form data to a session-backed store -- [ ] On field blur/change, POST the field name+value via HTMX (debounced 500ms) -- [ ] On page load, hydrate all fields from the draft endpoint -- [ ] Draft is cleared on successful submission; expires after 24h with session -- [ ] Add a visible "Brouillon enregistré" indicator to reassure the student - -### 4. FilePond temp file IDs lost on partage validation redirect - -**Current state:** After a validation error, `storePrimedFiles()` shows file *names* so the user knows what to re-select. But the actual FilePond hex IDs (server-side temp files) are not preserved — the user must re-upload everything. For large files on slow connections, this is painful. - -**To do:** -- [ ] After a validation error in the partage form, preserve FilePond temp file IDs in `$_SESSION['share_filepond_ids_' . $slug]` by queue type -- [ ] Use these IDs to build the `data-existing-files` JSON attribute on the re-rendered form, seeding the FilePond pools -- [ ] Ensure the temp files' server-side lifetime covers at least one validation round-trip (extend `FilepondHandler` TTL if needed) - -### 5. Two-phase commit: protect against crash during file moves - -**Current state:** DB transaction commits, then files are moved from tmp to storage. If the process crashes between COMMIT and file move completion, the DB has a thesis record with no files (or partial files). This is a public-facing service — no acceptable failure rate. - -**To do:** -- [ ] Add an `is_published` or `status` flag to theses table (if not already present) -- [ ] In the partage submit flow: INSERT thesis with `status='draft'` inside the transaction → COMMIT → move files → UPDATE `status='active'` -- [ ] On partage submission error recovery, roll back draft theses that are older than N minutes with no files -- [ ] Add a periodic cleanup job (`just cleanup-drafts`) for orphaned drafts - -### 6. Mobile-responsive form layout - -**Current state:** Form field rows use `grid-template-columns: 260px 1fr` with no breakpoint. Below ~520px viewport width, labels wrap and inputs are cramped. - -**To do:** -- [x] Add `@media (max-width: 600px)` rule in `form.css` switching to `grid-template-columns: 1fr` with labels stacked above inputs -- [x] Test on 320px wide viewport (PSP, low-end Android) — verified via responsive design tokens (min 360px clamp) -- [x] Ensure touch targets are at least 44×44px (WCAG 2.5.5) - -### 7. Split form.css: shared base vs admin-only styles - -**Current state:** `form.css` is 36KB (1,500+ lines). The partage form loads all of it but uses only ~40% (base input/fieldset styles). Admin-specific styles (`.admin-jury-*`, `.admin-backoffice-*`, etc.) are dead weight for students. - -**To do:** -- [ ] Split `form.css` into `form-base.css` (~15KB: inputs, selects, textareas, fieldsets, legends, validation states, flash messages, `aria-invalid` styling) and `form-admin.css` (~21KB: admin layout, jury fieldset, backoffice, tag pills, autocomplete) -- [ ] Load `form-base.css` in the partage form; load both in admin forms -- [ ] Update `head.php` to support `$extraCssAdmin` for admin-only stylesheets - -### 8. Fix `aria-required` on `
` (standards compliance) - -**Current state:** `checkbox-list.php` and `fichiers-fragment.php` put `required aria-required="true"` on `
` elements. The `required` attribute is not valid on `
`. Screen readers may not interpret this correctly. - -**To do:** -- [x] Remove `required` attribute from `
` in `checkbox-list.php` — keep `aria-required="true"` only -- [x] Remove `required` attribute from `
` in `fichiers-fragment.php` — keep `aria-required="true"` only -- [x] Add `role="group"` on both `
` elements for explicit ARIA semantics - -### 9. Refactor partage form page wrapper to a template - -**Current state:** `renderShareLinkForm()` in `partage/index.php` outputs an entire `` document inline (~120 lines of HTML inside a PHP function). This duplicates `head.php`/`header.php` logic. - -**To do:** -- [ ] Extract the partage form page chrome to `templates/partage/form-page.php` -- [ ] Use `App::render()` or a dedicated `renderPartageForm()` helper -- [ ] Share meta tags, favicon references, CSRF meta, and live-reload script from `head.php` - -### 10. Reduce form bootstrap duplication across 3 entry points - -**Current state:** `admin/add.php`, `admin/edit.php`, and `partage/index.php` (inside `renderShareLinkForm()`) each repeat ~40–60 lines of identical setup: load `ThesisCreateController`, call `loadFormData()`, build `$helpFn`, load site settings, set up jury/default access variables, etc. - -**To do:** -- [ ] Add a `ThesisFormSetup` helper class (or static method on the controllers) that returns a standardised array of form variables -- [ ] Each entry point becomes: `$formVars = ThesisFormSetup::prepare($mode, $slug); extract($formVars); include '...form.php';` +## Deferred / Blocked diff --git a/app/public/partage/index.php b/app/public/partage/index.php index 63af424..524d948 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -183,27 +183,11 @@ renderShareLinkForm($slug, $link); */ function renderShareLinkError(string $title, string $message): void { - $pageTitle = htmlspecialchars($title); + $pageTitle = $title; + include APP_ROOT . '/templates/partage/form-page.php'; ?> - - - - - - <?= $pageTitle ?> - - - - - - - - - - - @@ -258,24 +242,8 @@ function requirePasswordGate(array $link, string $slug): void $flashError = $_SESSION['_flash_error'] ?? null; unset($_SESSION['_flash_error']); + include APP_ROOT . '/templates/partage/form-page.php'; ?> - - - - - - <?= htmlspecialchars($pageTitle) ?> - - - - - - - - - - -

🔒 Accès protégé

@@ -335,8 +303,11 @@ function renderShareLinkForm(string $slug, array $link): void $_SESSION['csrf_token'] = $shareCsrfToken; } - $pageTitle = 'Soumettre un TFE'; - $isVerified = !empty($_SESSION['share_verified_' . $slug]); + $pageTitle = 'Soumettre un TFE'; + $includeFilePond = true; + $csrfToken = $_SESSION['csrf_token'] ?? null; + $filepondBase = '/partage/actions/filepond'; + $isVerified = !empty($_SESSION['share_verified_' . $slug]); // WCAG 3.3.1: which field has a validation error (set by submit handler via App::flashAutofocus) $errorFieldName = App::consumeAutofocus(); @@ -427,40 +398,9 @@ function renderShareLinkForm(string $slug, array $link): void $contactPublic = false; $currentContextNote = null; $currentContactVisible = null; + + include APP_ROOT . '/templates/partage/form-page.php'; ?> - - - - - - <?= htmlspecialchars($pageTitle) ?> - - - - - - - - - - - - - - - - - - - - - - - - - - -

diff --git a/app/templates/partage/form-page.php b/app/templates/partage/form-page.php new file mode 100644 index 0000000..3d30207 --- /dev/null +++ b/app/templates/partage/form-page.php @@ -0,0 +1,67 @@ + + * + * Optional variables: + * bool $includeFilePond — if true, includes FilePond CSS/JS + * array $extraCss — additional CSS files to load + * array $extraJs — additional JS files to load + * ?string $csrfToken — CSRF token for + * ?string $filepondBase — FilePond base URL for + */ +$includeFilePond = $includeFilePond ?? false; +$extraCss = $extraCss ?? []; +$extraJs = $extraJs ?? []; +$csrfToken = $csrfToken ?? ($_SESSION['csrf_token'] ?? null); +$filepondBase = $filepondBase ?? null; +?> + + + + + + <?= htmlspecialchars($pageTitle ?? 'XAMXAM') ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +