Commit Graph
401 Commits
Author SHA1 Message Date
Pontoporeia bfde71caaa style: fix biome check errors (import order + formatting)
npx biome check reported 14 errors on app/public/assets/css/,
app/public/assets/js/app/ and scripts/:
  - assist/source/organizeImports: scripts/css-content-sources.mjs,
    scripts/css-unused-report.mjs
  - format: 9 CSS files + 3 JS files

Fixed via biome check --write. Verified: npx biome check clean,
npm run build succeeds, PHPUnit 316 tests / 628 assertions pass.
2026-09-18 16:26:49 +02:00
Pontoporeia 554ba3ee8d fix(admin): stop logging out active long-form work; raise idle timeout to 4h
The admin idle timeout (30 min) was refreshed only by navigations and HTMX
requests. During long encoding sessions on an open form there are none, so
an actively-typing admin was logged out mid-work after ~30-45 min.

Add an activity-driven keepalive:
- /admin/session-keepalive.php: 204 when authenticated (refreshes
  admin_last_activity via AdminAuth::isAuthenticated()), 401 otherwise.
- admin-session-keepalive.js: marks activity only on real user input
  (pointer/keyboard/input/scroll/wheel/touch/focus) and pings at most once
  per 5 min while the tab is visible. A genuinely idle tab never pings, so
  the idle timeout still applies.

Raise the idle window 30 min -> 4 h: for a single-/few-admin back-office
whose main workflow is data entry, 30 min still kicked admins who stepped
away mid-form. With the keepalive in place, 4 h means "no interaction at
all", not "no navigation". Absolute timeout stays 12 h.

Also fix session ID rotation, which never fired: it used
`$absolute % IDLE_TIMEOUT_SECONDS === 0`, i.e. required a request to land
exactly on a multiple of the interval relative to login time. Replaced with
an explicit admin_last_rotation timestamp and a ROTATION_INTERVAL_SECONDS
(30 min) constant decoupled from the idle timeout, so raising the idle
window does not widen the fixation/replay window.

Refactor AdminAuth::enforceSessionTimeout() to return bool instead of
redirecting/exiting, so the keepalive endpoint can report 401 cleanly
rather than letting fetch follow a redirect to the login page.

Smoke test (just smoke-session-keepalive) covers activity refresh, 2 h idle
accepted, rotation firing, idle rejection+destruction, and unauthenticated
rejection. Docs updated.
2026-09-18 16:26:49 +02:00
Pontoporeia 25c5133086 perf(admin): infinite-scroll the contenus langues/mots-clés tables
The contenus page loaded both lookup tables in full via HTMX on page load:
760 tag rows + 216 language rows, ~2.4MB of HTML including ~1960 inline
SVG icons and per-row CSRF forms. The DB queries were already fast (~6ms);
the cost was pure client-side payload and DOM.

- Add paged lookups: getTagsPage/getLanguagesPage + countTagsWithCount/
  countLanguagesWithCount, and optional limit/offset on the existing
  unpaged variants (backward compatible).
- Fragments serve 25 rows per request. A sentinel <tr> with
  hx-trigger="intersect once root:#<wrap>" appends the next page when it
  scrolls into the table's own scroll container (htmx 'revealed' only
  checks the window viewport, not nested scrollers).
- Search forms still swap the whole wrapper and reset to offset 0.
- Style the load-more row; add PagedLanguagesTagsTest coverage.

Initial DOM for the page drops from ~2.4MB to ~160KB; scrolling reaches the
full totals (736 tags, 216 languages) and stops cleanly.
2026-09-18 16:26:49 +02:00
Pontoporeia 8016712741 fix(form): reword CC2r consent label to 'J'adhère au Collective Commitment to Reuse (CC2r)', italic 2026-09-18 16:26:49 +02:00
Pontoporeia 626970770f fix(toc): add trailing bottom spacing when the TOC scrolls
The desktop TOC is its own scroll container (overflow-y:auto + max-height),
but unlike article content — which gets trailing space via
.page-content > article::after — it had nothing below the last link. A long
TOC scrolled to the bottom left the final link flush against the edge.

Add .toc::after as a block pseudo-element inside the desktop media query,
using the same technique (and the same Firefox-clips-padding-bottom reason)
as the article. Shared component, so public (about/charte/licence) and admin
(#admin-toc) both get it. Verified with Playwright: 0px -> ~60px gap;
mobile unaffected (TOC is not a scroll container there).
2026-09-18 16:26:49 +02:00
Pontoporeia aa72aa5bc5 fix(admin): prevent access badge text wrapping in index table
Add white-space: nowrap to .status-access so 'Interne'/'Interdit'
no longer break mid-word in the narrow Accès column.
2026-09-18 16:26:49 +02:00
Pontoporeia beff95c784 fix(css): restore bottom spacing on content pages in Firefox
apropos/licence/charte use .page-content as a nested scroll container
(overflow-y:auto) inside html/body {height:100%; overflow:hidden} flex.
Firefox drops/clips that container's own padding-bottom (and last-child
margin) from the scrollable area -- scrollHeight came out ~80px short and
the last ~80px of content was unreachable. Chromium honours the padding.

Move the trailing space off the container padding onto a real content
spacer (.page-content > article::after, display:block; height) which both
browsers count as scrollable content. Verified via Playwright (Chromium +
Firefox): delta between article and scrollHeight is now 0 in both on all
three pages, consistent at all breakpoints. not-found unaffected (loads
its own not-found.css). 294 PHPUnit tests pass.
2026-09-18 16:26:49 +02:00
Pontoporeia 3f352b0d26 feat(admin): cleanup page — remove 'Fichiers temporaires' level, promote sections to h2 TOC entries 2026-09-18 16:26:49 +02:00
Pontoporeia bdde7173ea fix(ui): reserve scrollbar gutter in public TOC so it doesn't overlay text 2026-09-18 16:26:36 +02:00
Pontoporeia fc66b37801 feat(home): htmx lazy-load cover images
Replace the eager <img> on the home page with an htmx placeholder <figure>
that fetches a /cover-fragment endpoint when it scrolls into view
(hx-trigger="revealed"), so heavy cover bytes load only on demand.
Add spinner + settle-fade transition CSS, and load htmx.min.js on home.
2026-09-18 16:26:36 +02:00
Pontoporeia 541470b9bb feat(provision): idempotent setup for local dev and remote server 2026-09-18 16:26:36 +02:00
Pontoporeia 8accb88452 update: CSS per-page split and lightningcss unusedSymbols report
docs: record CSS template inventory + decide unusedSymbols-before-split ordering

feat(css): add content-source collector + dynamic-class safelist for unusedSymbols report

- scripts/css-content-sources.mjs: buildCorpus() gathers templates/public/src
  PHP + first-party js/app JS (vendor excluded), returns {corpus, sources,
  totalBytes, safelist, prefixes}
- Mined 22 exact runtime classes + 5 DB/state-derived suffix prefixes from
  status-badge.php, SystemController statusClass/logLineClass, and class=<?=?>
  ternaries
- docs/css-split-analysis.md notes content-corpus section
- td: #11 collect-content-sources done; feeds #12 report script

feat(css): unusedSymbols report script + just css-report recipe

- scripts/css-unused-report.mjs: per-bundle class/id extraction vs buildCorpus()
  corpus + safelists; measures reclaimable bytes via lightningcss transform
  unusedSymbols (report-only, no stripping to disk)
- just css-report: rebuild CSS then run the report
- css-content-sources.mjs: add VENDOR_CLASS_PREFIXES (filepond--*, htmx-*)
- RESULT: 216,383B total, ~6.2KB (2.9%) reclaimable; FilePond/HTMX exclusion
  corrected inflated 26% (56KB) false-positive down to honest 2.9%
- docs/css-split-analysis.md findings table + TODO 12/13 done

docs(css): record go/no-go decision — split NO-GO, pruning conditional-go

- Decision analysis in docs/css-split-analysis.md
- ~6.2KB (2.9%) reclaimable of 216KB; base.min.css only 484B (2.3%)
- SPLIT NO-GO: base.css already well-used; parked u/w/x/y(/z) as deferred
- PRUNING conditional-go on hand-verifiable dead selers from source, never dist;
  re-run just css-report after each edit; keep needs-review + vendor-prefix cls
- td: task 14 done; split stream 41 tasks -> 2 pending / 32 done / 7 deferred

todo: defer CSS pruning stream (10/u/w/x/y/z), context updated
2026-09-18 16:26:36 +02:00
Pontoporeia a31e4ab307 Recherche insensible aux accents sur toutes les barres de recherche (publique + backoffice).
Ajoute une fonction SQLite UDF accfold() (app/src/AccentFolding.php) qui plie les caractères accentués latin vers leur base ASCII (é→e, ç→c, …), et l'applique à chaque condition de recherche côté colonne ET côté terme (accfold(column) LIKE accfold(:term)). Comme le runtime n'a ni intl (Normalizer) ni iconv, le repli passe par une map de translitération manuelle (Western Latin-1 + Latin Extended-A, NULL-safe).

Couvert :
- recherche publique (searchTheses / countSearchResults) : titre, sous-titre, synopsis, auteurs, promoteurs, tags ;
- popover étudiant (getThesesByAuthorName / getThesesForAuthors) : a.name ;
- recherche backoffice (getThesesList / getThesesListCount) : titre, sous-titre, identifiant, auteur.

L'UDF est enregistré dans Database::registerSqliteFunctions() (appelé par le constructeur et par le harnais de test TestDatabaseInstance). Évite aussi la dépréciation PHP 8.5 de PDO::sqliteCreateFunction() : la connexion est créée via \Pdo\Sqlite quand disponible (createFunction()) avec repli sur PDO (sqliteCreateFunction()) sur les anciens runtimes ; les tests passent de @dataProvider docblock à l'attribut #[DataProvider] (fin de la dépréciation PHPUnit). Détache .php-cs-fixer.cache (gitignoré) pour ne plus polluer le working copy.

294 tests PHPUnit verts sans dépréciation, phpstan niveau 5 OK.
2026-09-18 16:26:36 +02:00
Pontoporeia 0896c4b8c8 Synopsis : normalisation SÛRE des text ajouté par formulaire
- fins de ligne \r\n/\r → \n,
- blocs de lignes vides → \n\n, è
- espaces/tabulations/NBSP → espace unique,
- trim.
Les retours à la ligne simples intra-paragraphe sont LAISSÉS INTACTS :
une coupure de ligne peut être une vraie frontière de mot (→ espace)
ou une coupure du mot ("dyna\nmiques" → "dynamiques"),
indistinguables sans dictionnaire — automatiser corromprait l'entrée
("poursuivantsur").

Migration batch 045 (backfill des données existantes) RETIRÉE : le
nettoyage automatique fiable des synopsis existants est impossible.
Chaque modification est testée (277 tests PHPUnit).
2026-09-18 16:26:36 +02:00
Pontoporeia 6e1fc6a781 Add admin-only route to open Interdit thesis files from backoffice:
- reliable tab title /favicon wrapper,
- Content-Disposition filename,
- admin media route hardened to thesis-file prefixes only (defense-in-depth)
2026-09-18 16:26:36 +02:00
Pontoporeia e518163c5b Style links on licences, charte, a propos pages as real links (underline + accent hover) 2026-09-18 16:26:36 +02:00
Pontoporeia 116dca5d75 Admin: search by thesis identifiant (id) in backoffice list 2026-09-18 16:26:36 +02:00
Pontoporeia 61b105da27 feature: Lien BAIU + thesis ID in tfe.php 2026-09-18 16:26:36 +02:00
Pontoporeia 307988eb3c feat: enforce idle + absolute timeouts on admin session 2026-09-18 16:26:36 +02:00
Pontoporeia 30a16f9e9e fix: display db timestamps in Brussels time
(heure de dépôt was showing UTC)

- feat: add date_depot column (real TFE deposit date)
with CSV round-trip + Brussels→UTC sanitization
- fix: keep PHP default tz at UTC to preserve token/share-link
 expiry consistency; convert to Brussels only in db_datetime()
2026-09-18 16:26:36 +02:00
Pontoporeia 2232981753 fix tfe.php scroll: make .tfe-main a flex container so the height chain resolves
.tfe-main (main) is a block container by default, so .tfe-layout's
height:100% and .tfe-content-row's flex:1 never resolved to a
definite height. The columns' overflow-y:auto thus had nothing to
constrain, and main's overflow:hidden clipped the content — scroll
was visually broken. Adding display:flex lets the column/height
chain resolve so both columns scroll independently as intended.
2026-08-24 11:36:15 +02:00
Pontoporeia f31addb6bc add password-reset smoke test + AdminAuth DI + OneTimeToken empty-context redeem fix 2026-08-24 11:36:02 +02:00
Pontoporeia ddae5d8fef add password-reset flow: request endpoint + reset page + login link (shared OneTimeToken) 2026-08-24 11:36:02 +02:00
Pontoporeia d7184e4447 migrate file-access tokens onto shared OneTimeToken model (hash-at-rest) 2026-08-24 11:36:02 +02:00
Pontoporeia abb735253e extract shared OneTimeToken model for single-use tokens + AdminAuth reset API 2026-08-24 11:35:22 +02:00
Pontoporeia 802b601b59 admin parametres: remove the remove-credentials danger zone 2026-08-24 11:35:22 +02:00
Pontoporeia c205a00c3f admin parametres: hr separator between current/new password, drop new-password top border 2026-08-24 11:35:22 +02:00
Pontoporeia 138b59f66c admin parametres: add shared Identifiants section, trim Emails, restructure PeerTube fieldsets
- admin parametres: move Compte administrateur into Identifiants as a fieldset
- admin parametres: space the account fieldset and drop confirm-password top border
- admin parametres: tighten param-form row spacing, relabel password button
- admin parametres: move password submit button outside the account fieldset
2026-08-24 11:35:22 +02:00
Pontoporeia fb5e856288 admin: backup logs via parameters.php, nextcloud secondary backup
- surface backup/cleanup cron logs + backup freshness status
- email xamxam@erg.be when SQLite backups go stale (backup watchdog)
- sync SQLite snapshots to Nextcloud WebDAV + remote-freshness watchdog
- precise retention pruning, manual sync in check recipe, and Nextcloud-sync docs
2026-08-24 11:34:57 +02:00
Pontoporeia 3e721b19f0 ui: system logs style update
- Increase padding in admin log dropdown selects
- Move admin log filters above the Journaux tabs
2026-08-24 11:34:34 +02:00
Pontoporeia d2cef85966 logs: standardise log filenames to xamxam-{service}-{date}.log 2026-08-24 11:34:34 +02:00
Pontoporeia 7b6d79c133 diag: invalid_grant is SSO auth-method mismatch, not bad creds
- feat: creds-test.sh gum probe for SMTP vs PeerTube auth + PeerTubeService::probeAuth()
- feat: app-token.sh gum probe for long-lived PeerTube app token (client_credentials)
- docs: add copy-paste proof commands to demonstrate the SSO break to admins
2026-08-24 11:33:34 +02:00
Pontoporeia 5460041989 feat: allow exporting an empty CSV template for imports 2026-08-24 11:33:34 +02:00
Pontoporeia 7938ab39c0 feat: add custom 404 page rendered via the site layout 2026-08-24 11:32:02 +02:00
Pontoporeia e9747edce0 docs: verify and refactor documentation to match current codebase 2026-08-24 11:31:38 +02:00
Pontoporeia b2cdbd0174 fix: CSV import VALUES placeholder count mismatch (27 values for 26 columns) 2026-07-10 19:07:19 +02:00
Pontoporeia 64323fd17e sort TFE files by display category on public page: note d'intention → TFE → image → video → audio → website → annexes 2026-07-10 19:07:14 +02:00
Pontoporeia 6e1d54511d fix: remove redundant query= param from filter-specific metadata links on TFE page
Clicking orientation, AP program, finality, year, format, or keyword links
on the TFE page was passing both a dedicated filter param AND a query=
param. The query= text search doesn't look at the filter columns
(orientation, ap_program, etc.), so the AND combination yielded zero
results when the filter value didn't appear in title/authors/etc fields.

Also fix the language link: it was passing language as query= which
searches only title/authors/etc, not the languages column. Now uses
the dedicated language= param (DB layer already supported it, and
collectSearchParams now collects it).
2026-07-10 17:20:21 +02:00
Pontoporeia 14c21586c5 fix: clamp student popover position to stay within viewport vertically 2026-07-10 17:11:10 +02:00
Pontoporeia d8f85b03ad fix: use getElementById for relink FilePond pool lookup
FilePond 4.x wraps the original input element in its own DOM structure
which can break CSS attribute selectors. querySelector with
.tfe-file-picker[data-queue-type='...'] returned null even though the
element existed and was initialized.

Switched to getElementById with a queueType→inputId map:
  cover → couverture
  note_intention → note_intention
  tfe → tfe-files-input
  annexe → annexe-files-input

getElementById is both faster and immune to FilePond's DOM wrapping.
2026-07-10 16:42:56 +02:00
Pontoporeia 6da45435f5 fix: relinked files now appear in FilePond UI immediately
Switched from pond.addFile(type:'limbo') to pond.addFiles([{source, options:{type:'local'}}])
in both XamxamRelinkFile and XamxamRelinkPeerTube.

addFile with type 'limbo' doesn't trigger server.load to fetch the file
for the preview/thumbnail. The data-existing-files format (type:'local')
correctly tells FilePond to call server.load for the file identified by
the source (DB ID), rendering it properly in the pool.
2026-07-10 16:36:30 +02:00
Pontoporeia 7013f79fc0 fix: merge corbeille lists + bulk restore button in cleanup
- Single unified 'Corbeille' table replaces the two separate lists
  (stale/orphelin + restorable). Each row has a Statut column showing
  '↺ Restaurable' or 'Orphelin', plus a checkbox for bulk ops.

- Checkboxes on restorable rows carry data-restorable='1' attribute
  so the bulk JS can distinguish them.

- Bulk actions bar now has both 'Supprimer la sélection' and
  '↺ Restaurer la sélection' buttons. The restore button only appears
  when at least one restorable item is checked.

- New cleanupBulkRestore() JS function populates a dedicated hidden
  form and confirms before submitting. Only restorable items are sent.

- restore-trash.php refactored to handle both single (trash_file) and
  bulk (trash_files[]) inputs via a loop, accumulating restored/skipped
  counts and re-rendering the fragment on HTMX requests.
2026-07-10 16:33:06 +02:00
Pontoporeia 3cecee10c9 fix: prevent file deletion on relink + restore button now visible + OOB-style in-place update
Two critical fixes:

1. Relink flow no longer destroys/recreates FilePond instances:
   The relink (XamxamRelinkFile) and PeerTube relink (XamxamRelinkPeerTube)
   previously refreshed the entire fichiers fragment via HTMX after
   pond.addFile(). This triggered destroyFilePondsIn on ALL pools, which
   could fire server.remove callbacks and move existing files to corbeille.
   Now just closes the modal — the file is already added to the pool in-place,
   and syncOrderInput creates the hidden form input.

2. Cleanup page « Corbeille (restaurable) » now actually shows files:
   _cleanup-stats-data.php previously classified trash files by checking if
   the thesis_files DB row still existed. But both deleteThesisFileToTrash
   and FilepondHandler::handleRemove DELETE the DB row. So ALL trash files
   appeared as `stale` (not restorable). Now uses the JSON sidecar file
   presence as the classification criterion — if the sidecar exists and is
   recent, the file is restorable regardless of DB row state.

Also removed unused DB query from _cleanup-stats-data.php.
2026-07-10 16:29:04 +02:00
Pontoporeia ed19e30cf0 repertoire keyword column: smaller font (step--1), tighter line-height (1.15), no hyphenation 2026-07-10 15:59:48 +02:00
Pontoporeia 0045c06fef fix: prevent keyword word-breaking in repertoire columns 2026-07-10 15:51:03 +02:00
Pontoporeia c1a05efcda fix: bypass /tmp tmpfs for file uploads by redirecting PHP upload_tmp_dir to storage partition 2026-07-10 15:42:07 +02:00
Pontoporeia 1293b1be6c remove TOC temporary debug error_log calls 2026-07-10 15:23:52 +02:00
Pontoporeia c67262673f paramètres: fix logs Copier button scrolling out of view on horizontal scroll
Wrap .log-output and the copy button in a .log-output-wrapper div
with position:relative, so the button sits outside the overflow-x:auto
scroll container and stays fixed at the top-right corner.
2026-07-10 15:21:29 +02:00
Pontoporeia 3e3d829d11 fix: website iframe overlay — icon button bottom-right, no blur, tooltip 2026-07-10 15:18:42 +02:00
Pontoporeia 3d5d972ad0 fix TOC invisible: add open attr to details.toc — extractToc works but closed details + desktop pointer-events:none hid content; also make apropos TOC dynamic
fix: website iframe overlay — icon button bottom-right, no blur
2026-07-10 15:18:42 +02:00