Commit Graph
146 Commits
Author SHA1 Message Date
Pontoporeia 56b3feb73f extend admin auth cookie lifetime from session-only to 1 week (604800s) 2026-07-10 11:56:27 +02:00
Pontoporeia 22a49f7cb6 fix: unify CSV import/export columns, fix import JS error, fix createThesis VALUES
- Fix createThesis SQL: extra ? placeholder in VALUES (27 values for 26 columns)
- Add createThesis integration tests to catch column/value mismatches
- Add CSV_COLUMNS as single source of truth in ExportController, deriving
  csvHeaders() and positional fallback from it
- Add missing columns to export query + CSV: duration_pages, duration_minutes,
  has_annexes, license_custom, contact_visible, objet
- Add missing columns to import INSERT: license_id, license_custom, cc2r,
  exemplaire_baiu, exemplaire_erg, objet, contact_visible, duration_pages,
  duration_minutes, has_annexes
- Resolve license name → license_id during import
- Fix XamxamInitFilePonds: add file-upload-filepond.js to admin-entry.js
  so FilePond initialization code is available on the admin list page
- Add FilePond vendor CSS to admin.min.css bundle (import dialog styling)
- Generate .admin-file-hint from csvHeaders() instead of hardcoded stale list
- Use positional fallback from CSV_COLUMNS for import cell parsing
2026-07-10 11:56:27 +02:00
Pontoporeia 58fb4142b7 relabel: Promoteur·ice ULB → Promoteur·ice université in all UI labels
Pure label change across 6 template/controller files — no DB schema change.
Migration 043 added to track the change.
2026-07-06 19:51:42 +02:00
Pontoporeia 0979267c68 stretch heading scale (h1→step-5, h3→step-1) + reduce heading bottom margin (space-l → space-m) 2026-07-06 19:51:20 +02:00
Pontoporeia 10df92b643 fix: load autosave-handler.js on about_page edit view, persist contact group deletion
Two fixes:

1. about_page edit was missing autosave-handler.js: the first if block
   (editType === 'page' || 'about_page') overrode $extraJs with only
   overtype.min.js, making the elseif that added autosave-handler.js
   unreachable for about_page. Added autosave-handler.js to the first
   block, removed dead 'about_page' from the elseif.

2. Deleting a contact group (Contacts 3/4/5) only removed the DOM
   element but never triggered htmx autosave. Dispatched a 'change'
   event on the form after removal+reindex so the hx-trigger fires.

Also expanded autosave-handler.js URL filter to accept apropos.php
so the status indicator (Enregistrement/Enregistré) works for contacts saves.
2026-07-05 14:14:13 +02:00
Pontoporeia 8e309bbcb7 Remove public contact visibility checkbox from admin add/edit forms 2026-07-05 14:04:29 +02:00
Pontoporeia 3e93150c76 justfile: standardise test recipes to lint-php/lint-css/lint-js/test + add fix recipe
- Removed ambiguous aliases: phpstan, cs-check, syntax
- Split lint-biome into lint-css and lint-js with correct paths
- Added lint meta-recipe and fix recipe (biome --unsafe + php-cs-fixer)
- Fixed FormBootstrap dead null check, CSS shorthand override bug
- Updated phpstan baseline, suppressed noDescendingSpecificity/noInnerDeclarations
- Applied ~74 biome auto-fixes across CSS/JS
2026-07-05 11:07:41 +02:00
Pontoporeia 34b5d3e585 justfile: standardise test recipes to lint-php/lint-css/lint-js/test + add fix recipe
- Removed ambiguous aliases: phpstan, cs-check (both pointed to lint-php which
  also ran php-cs-fixer, making the names misleading)
- Removed syntax (php -l) — redundant, phpstan already catches parse errors
- Split lint-biome into lint-css and lint-js with correct paths
- Added lint meta-recipe that runs all three linters
- Added fix recipe: biome check --write (CSS/JS format+lint) + php-cs-fixer fix (PHP)
- Updated build-lint to delegate to lint-css + lint-js
2026-07-05 10:50:42 +02:00
Pontoporeia 880d169e72 search: pagination and scroll, add range counter, mobile version 2026-07-03 16:53:16 +02:00
Pontoporeia 4fa57d592a refactor: combined duration (pages + minutes), has_annexes checkbox, remove Mo
- Remove Mo option from duration, keep only pages and minutes
- Redesign duration fieldset: separate Pages input + Durée h:m inputs, both can be set together
- Fix minutes input visibility: wider inputs (6ch), proper CSS layout
- Add has_annexes checkbox to fichiers fragment + DB column + controllers
- Display duration on admin backoffice recap page
- Display duration on public partage recap page
- Update public TFE page for new combined duration format
- Migration 041: add duration_pages, duration_minutes, has_annexes columns; migrate data; recreate views
2026-07-03 15:58:10 +02:00
Pontoporeia e6a989bb36 fix: add missing bootstrap require in markdown-cheatsheet-fragment.php, add bottom padding to dialog 2026-07-03 15:24:07 +02:00
Pontoporeia 0062b29678 Fix security issues from audit: gate partage fragments on share_active session, add CSRF to retry-email POST, remove dead App::verifyCsrf() 2026-06-24 14:17:24 +02:00
Pontoporeia 20fe4b6c8c Add biome + rolldown + lightningcss build pipeline for JS/CSS bundling & minification
- package.json with biome, rolldown, lightningcss devDependencies  
- biome.json: add CSS formatter support
- scripts/build-css.mjs: lightningcss resolves @import chain, bundles/minifies CSS  
- scripts/build-js.mjs: rolldown per-entry JS bundling (no code splitting)
- scripts/build.mjs: orchestrator for both CSS + JS
- scripts/check-build.mjs: staleness checker for CI/deploy guard
- justfile: add build, build-css, build-js, build-install, build-check recipes
- justfile: deploy recipe now runs build before deploy-code
- head.php + form-page.php: use dist/base.min.css instead of style.css
- All controllers + FormBootstrap: reference dist/*.min.{css,js}
- admin footer: load admin.min.js for all admin pages
- repertoire: use public.min.js instead of individual app JS files
- Fix stray '}' syntax error in admin.css line 305
- .gitignore: add app/public/assets/dist/
2026-06-24 13:09:50 +02:00
Pontoporeia cca3d08f05 Replace apropos/licence/charte sticky nav with responsive details/summary TOC
- Convert .apropos-toc <nav> to <details class="toc" open> in all three templates
- Add caret-down icon to summary (visible only on mobile via media queries)
- Desktop (≥768px): sticky sidebar via CSS grid, force-open via pointer-events:none, hide caret
- Mobile (≤767px): single column, collapsible TOC with rotating caret, margin-top below search bar
- Rename .apropos-toc-link to .toc-sidebar-link
- Merge 900px and 600px breakpoints into single 600px mobile media query
- Rename apropos.css → content-page.css; update 3 controller references
2026-06-22 15:33:47 +02:00
Pontoporeia dfde88eaa5 Migrate all &lt;img&gt;-based icons to inline SVG via PHP helper
Replace every &lt;img src="/assets/icons/..."&gt; with &lt;?= icon('name') ?&gt;
across 26 template files. The PHP helper inlines the SVG markup into the
DOM so CSS color cascades naturally through fill="currentColor".

- Add src/icon.php helper: reads SVG file, sets width/height to 1em,
  injects aria-hidden, supports optional CSS class
- Fix 12 icon SVGs that had hardcoded fill="#000000" or missing fill attr
- Replace search.svg with Phosphor fill-based magnifying glass
- Add explicit SVG sizes for admin header nav icons (16px/20px)
- Scope public search icon CSS to form[role=search]:not(.header-search-form)
  to avoid breaking admin header layout; change stroke to fill
- Remove &lt;img&gt; filter: brightness(0) invert(1) hacks from admin.css
2026-06-21 17:52:27 +02:00
Pontoporeia 03c9c3566f Add SQLite indexes for contenus page language/tag queries + WIP: Peertube orphans, dialogs, contact decoupling, context note, finality types 2026-06-21 16:36:34 +02:00
Pontoporeia 0d5e9dac19 fix: make sticky TOC work for full scroll height and fix heading anchor links 2026-06-20 14:08:16 +02:00
Pontoporeia 19bf9f101a Refactor apropos/charte/licence pages: shared layout, TOC anchors, and UI polish
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
2026-06-19 19:40:05 +02:00
Pontoporeia a5db81a73d fix: change adminOld return type from string to string|array
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.
2026-06-15 16:17:29 +02:00
Pontoporeia d588ae004d Reintroduce TFE duration metadata: DB columns, form fields, controllers, views, and migration
Add 'unsafe-eval' to CSP script-src directives (htmx requires Function())
2026-06-15 15:56:52 +02:00
Pontoporeia 00fed5f0e3 Add periodic cleanup of orphaned drafts: cleanup job, just command, deploy cron 2026-06-11 13:05:34 +02:00
Pontoporeia a19e9e1454 Extract FormBootstrap helper to eliminate bootstrap duplication across add/edit form pages 2026-06-11 12:23:55 +02:00
Pontoporeia f4a3e26901 Add thesis status column for two-phase commit lifecycle tracking 2026-06-11 12:09:43 +02:00
Pontoporeia cbd369bc72 Split form.css into form-base.css and form-admin.css, drop dead upload-progress code
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.
2026-06-11 11:28:58 +02:00
Pontoporeia 99125cc8e3 Add autosave draft system for partage form with HTMX-based session persistence
- 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)
2026-06-11 11:04:49 +02:00
Pontoporeia 4b37a05be3 Guard no-JS file uploads: disabled filepond_mode by default, server-side fallback
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.
2026-06-11 10:32:50 +02:00
Pontoporeia 2829d13a16 filepond: fix crash 'can't access property main, n.status is undefined'
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).
2026-06-10 00:18:49 +02:00
Pontoporeia 4a2b000fca Add Charte static page (public + admin editing) 2026-06-10 00:18:49 +02:00
Pontoporeia 317547ac93 Fix #4 v2: decouple contact_interne from contact_visible in ThesisCreateController
validateAndSanitise() no longer cross-contaminates:
- contact_interne overwrote mail, which then copied to contact_visible
- Fixed: contactInterne from contact_interne (admin) or confirmation_email (student)
- Fixed: contactVisible from contact_visible (admin) or mail (student)
- Fixed: submit() uses contactInterne as author email, not mail
2026-06-10 00:18:49 +02:00
Pontoporeia 1490c99268 Fix FilePond: maxFileSize as bytes + temp files survive page reload
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.
2026-06-10 00:18:49 +02:00
Pontoporeia c4a550f9d1 Rework contenus-edit: auto-save, OverType toolbar, dynamic sidebar links
- 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
2026-06-10 00:18:40 +02:00
Pontoporeia 021c58925e fix: auto-regenerate thesis identifier on any year-prefix mismatch, support .php migrations in runner
ThesisEditController::save() previously only regenerated the identifier when
the year field changed during an edit. If a thesis had its year corrected in
a past edit (or via other means) and the identifier still carried the old
year prefix, subsequent edits that didn't touch the year field would leave
the mismatched identifier in place.

Now saves() also checks whether the existing identifier's 4-digit prefix
matches the thesis year, and regenerates if not — regardless of whether year
changed in the current edit.

The migration runner (run.php) only scanned for .sql files, so PHP migrations
(013, 016, 018, 038) were never auto-applied. Extended the runner to also
discover and execute .php migrations in a subprocess. If a PHP migration fails
with an idempotent error (no such column, already exists, duplicate column),
the runner treats it as already-applied and continues rather than aborting
— preventing a stale migration like 016 (banner_path already dropped by 028)
from blocking migrations that come after it alphabetically (e.g. 038).

Updated migrations 016 and 038 to accept an optional $argv[1] DB path.
Fixed 016 to gracefully handle the banner_path column already being gone
(exit 0 instead of fatal).
2026-06-10 00:17:30 +02:00
Pontoporeia 07370b7221 search: ajout filtres finalité et format, boutons plus compacts et Réinitialiser en neutre 2026-06-10 00:17:00 +02:00
Pontoporeia 3df1456781 fix: author name casing not updating — use ID lookup priority
Root cause: SQLite uses BINARY collation, so WHERE name = ? is
case-sensitive. When changing 'john doe' to 'John Doe', the name
lookup failed and fell through to the email path which didn't update
the name. The previous fix only added UPDATE in the name-match branch.

Fixes in findOrCreateAuthor:
1. Accept optional $idHint parameter — when known (edit flow), update
   directly by ID (fastest, zero ambiguity)
2. Add COLLATE NOCASE to the name lookup (fallback path)
3. Add UPDATE in the email fallback path too

setThesisAuthors now fetches existing author_ids before deletion and
passes them as position-based hints, so identity is always preserved.
2026-06-10 00:17:00 +02:00
Pontoporeia 3016c199bd Fix edit form: is_published reset, contact decoupling, note label, author name case
- Fix #1: Add is_published to getThesisRawFields() SELECT so the publish
  checkbox stays checked when editing an already-published TFE.
- Fix #2: Rename 'Note contextuelle' → 'Note contextuelle relative à
  soutenance' in all templates and StudentEmail.
- Fix #3: Update findOrCreateAuthor to also UPDATE the author name when
  a record is found by name (fixes inability to capitalise names).
- Fix #4/#5: Decouple contact_interne (private author email) from
  contact_visible (public contact on TFE page). Add migration 037 to
  add contact_visible TEXT column to theses table and rebuild
  v_theses_full view. Update all controllers, templates, and DB methods
  to treat them independently.
- Fix #6: Investigated libre→interne restriction — no code barrier
  found; likely resolved by is_published fix.
2026-06-10 00:17:00 +02:00
Pontoporeia 3d524226a1 formulaire: correctifs identifiant/année, contact, fichiers optionnels
- 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)
2026-06-10 00:17:00 +02:00
Pontoporeia a2092b58a7 fix: supprimer les vidéos PeerTube lors de la suppression d'un TFE
- Ajout de PeerTubeService::deleteVideo() qui appelle DELETE /api/v1/videos/{uuid}
- deleteThesisFileToTrash() appelle maintenant deleteVideo() pour les fichiers peertube_ids:
- hardDeleteThesis() supprime aussi les vidéos PeerTube associées
2026-06-10 00:16:22 +02:00
Pontoporeia 312d9eab0e À propos: contacts flexibles, liens sidebar éditables, grille contacts admin, et bouton supprimer
- Contacts: on peut laisser vide le nom OU le rôle (plus besoin des deux)
- Sidebar: les liens « site de l'erg » et « code source » sont éditables depuis /admin/contenus-edit.php?slug=about
- Admin: les champs Nom/Email/Lien des contacts s'affichent en grille 3 colonnes
- Admin: icône corbeille (admin-icon-btn--delete) pour supprimer un contact, avec réindexation automatique
- Database::getAproposContent() gère maintenant les valeurs string (URLs) en plus des arrays
- Database::saveAproposContent() accepte array|string
2026-06-10 00:16:22 +02:00
Pontoporeia 71949425c7 TFE page: remove underlines from all links, lowercase keywords/languages/formats, inclusive writing, prevent keyword mid-word breaks, editable restriction messages in admin 2026-06-10 00:15:41 +02:00
Pontoporeia df70fba5d4 feat: convert all file inputs to FilePond for standardized uploading
- 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
2026-06-09 13:12:22 +02:00
Pontoporeia f398a0f1ff Fix non-constant-time credential comparisons
- account.php: replace !== CSRF token check with hash_equals
- ShareLink::setPassword(): also encrypt and store plain-text password
  alongside the hash, matching create() behavior so the decrypted_password
  decoration stays correct after password updates
2026-05-31 17:49:43 +02:00
Pontoporeia 6246174fc5 Export: add LINK.txt with PeerTube watch URLs to file ZIP export 2026-05-31 17:46:02 +02:00
Pontoporeia ae66c2baad Integrate Monolog: replace four logging systems with single PSR-3 factory
- Add monolog/monolog dependency (^3.10)  
- Create app/Logger.php central factory with channels: app, admin, error, audit
- Each channel gets RotatingFileHandler (30-day retention) with pass-through LineFormatter
  preserving existing JSON format contracts
- Rewrite AppLogger as thin facade delegating to Logger::get('app')
- Rewrite ErrorHandler::log() to delegate to Logger::get('error')
- Rewrite AdminLogger file output to delegate to Logger::get('admin'), keep DB writes
- Add Monolog file shadow to Audit via Logger::get('audit') (Option A per monolog-plan)
- Log level controlled by LOG_LEVEL env var (defaults: DEBUG in cli-server, WARNING otherwise)
- Graceful NullHandler fallback when log directory is not writable
- Update SystemController LOG_FILES: remove php_error, add app/admin/error/audit
- JSON app logs parsed to readable one-liners in the log viewer
- Remove nginx config tab (parametres + fragment + template + css)
- Friendly empty-state message when app log files don't exist yet (notYet)
- PHP tail fallback when exec() unavailable
- All 228 PHPUnit tests pass, no call sites changed
2026-05-20 12:28:31 +02:00
Pontoporeia d9e4541749 Remove unused Parsedown.php (replaced by league/commonmark in Phase 1); update phpstan baseline 2026-05-20 01:21:47 +02:00
Pontoporeia a0cda5b55d Phase 3: Replace SmtpRelay SMTP socket with PHPMailer 2026-05-20 01:17:44 +02:00
Pontoporeia ba57820016 Phase 2: Replace PeerTubeService HTTP client with Guzzle 2026-05-20 01:07:41 +02:00
Pontoporeia 5e75cacad7 Phase 1: Replace Parsedown with league/commonmark (4 call sites) 2026-05-20 01:02:09 +02:00
Pontoporeia 728f05502c Combine phpstan, cs-check, cs-fix into lint-php recipe; fix lint issues + test failures + duplicate detection bug 2026-05-20 00:31:19 +02:00
Pontoporeia defc919cd0 cleanup modal: list stale files to remove; storage restructure: documents/ → {objet}/ 2026-05-19 23:58:51 +02:00
Pontoporeia 678f9fc804 Index page: remove Mots-clés button, move export to bulk selection, fix ZipArchive error, move DB export to paramètres, sticky thead
- Remove 'Mots-clés' button from toolbar (redundant with admin sidebar tags)
- Replace export dialog with 'Exporter CSV' + 'Exporter fichiers' buttons in bulk selection bar
- Export dispatcher now accepts ?ids=1,2,3 for per-selection export
- All ExportController/Database methods accept optional thesisIds array
- Graceful error message when ZipArchive extension is missing on server
- Move DB export (SQLite download) to paramètres → Maintenance section
- Sticky table column headers (position: sticky, top: 0, z-index: 5) for index page table
2026-05-19 23:58:51 +02:00