Commit Graph
100 Commits
Author SHA1 Message Date
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 a45a2c9ac4 Changed dev serve recipe to be just dev 2026-06-10 00:17:41 +02:00
Pontoporeia 655dd4c038 feat: clarification contact étudiant + déplacer Contact visible dans Informations du TFE
- Label : « Contact visible (optionnel) », placeholder : mail/site/insta/etc.
- Hint : demander l'URL complète, le système raccourcit à l'affichage
- Affichage public (tfe.php) : extraction d'identifiant depuis l'URL
- Déplacement de contact_visible du Backoffice vers le fieldset Informations du TFE
- Renommage « Identité » → « Informations du TFE » dans le récapitulatif admin
2026-06-10 00:17:41 +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 34739d6ae5 feat: migration 038 to fix thesis identifiers mismatched with their year 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 c4664ec2e9 fix: prevent mid-word break in repertoire column headers
Base.css applies word-break: break-word to all elements inside <main>,
causing mid-word breaks in narrow columns. Override in repertoire.css:
- hyphens: none, word-break: normal, overflow-wrap: normal on all h2
- redistribute grid fractions: shrink Orientations (1.2→0.9fr), 
  Étudiantes (1→0.8fr), boost Finalité (0.7→0.9fr, min 7rem)
2026-06-10 00:16:49 +02:00
Pontoporeia a184e0d253 Ajouter l'affichage de la finalité du master sur la page publique TFE 2026-06-10 00:16:22 +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 a1a9a316ca rework tfe.php layout: row1 author above title, row2 meta+synopsis 2-col grid, row3 flex files 2026-06-10 00:16:10 +02:00
Pontoporeia e0d706c677 tfe.css: tfe-meta-item font-weight 400→700 2026-06-10 00:15:41 +02:00
Pontoporeia c9fa5943cf repertoire: rep-entry + col h2 step-0, years col step-3 2026-06-10 00:15:41 +02:00
Pontoporeia ef6bff895a admin nav-logo: grid layout for icon+text horizontal alignment and vertical centering 2026-06-10 00:15:41 +02:00
Pontoporeia 9e272873e1 style: set tfe-meta-item default to font-weight 400 so Accès/Licence values render at regular weight 2026-06-10 00:15:41 +02:00
Pontoporeia 3588f22d7b style: consolidate aria-current nav styles — remove border-radius from base header links, keep global :focus-visible ring, move border-bottom/padding to shared header.css 2026-06-10 00:15:41 +02:00
Pontoporeia cb2b18e470 style: standardise links to Regular weight (400) with violet accent hover, body to Light (300) 2026-06-10 00:15:41 +02:00
Pontoporeia cee3345ea3 tfe.php: afficher CC2r + licence, formater contact court, supprimer download PDF 2026-06-10 00:15:41 +02:00
Pontoporeia 24b753a992 fix: add missing csrf_token to htmx checkbox in file access restrictions
The 'Activer la restriction d'accès' checkbox in /admin/acces.php used
htmx to POST to settings.php but the #fieldset-restrictions container
was missing a csrf_token hidden input. This caused two bugs:
1. 'Erreur de sécurité, token invalide' error
2. Full /admin/parametres.php HTML injected into #restrictions-response
   (due to HTMX following the 302 redirect on CSRF failure)
2026-06-10 00:15:41 +02:00
Pontoporeia 3f200dae70 TFE page: replace dl/dt/dd with p/span for metadata, remove underlines, lowercase keywords/languages/formats, inclusive text, editable restriction messages 2026-06-10 00:15:41 +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 9a8f0cad65 fix(répertoire): colonnes différenciées, scrollbars discrètes, fontes conformes maquette, AP entre crochets
- grid-template-columns: années=0.4fr, orientations=1.2fr, AP/finalité/intermédiaires
- scrollbars: WebKit 5px transparent + Firefox scrollbar-width:thin global
- rep-entry: BBBDMSans Regular 398, --step--1
- années: BBBDMSans Medium 498 (semi-bold)
- titres colonnes: Ductus Regular 398, text-secondary, letter-spacing 0.12em
- AP: diminutifs entre crochets (ex: Design et Politique du Multiple [DPM])
- TODO: marquer les 4 correctifs répertoire comme faits
2026-06-10 00:15:33 +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 fad38f4e0d Added more logs to be excluded 2026-06-08 10:25:24 +02:00
Pontoporeia 053f09b181 fix(migration): deduplicate languages before LOWER() in 025_lowercase_languages.sql
Two rows (Néerlandais id=5, néerlandais id=3) collided when lowercased,
violating the UNIQUE constraint on languages.name.

Added DELETE to keep the lowest-ID row per LOWER(name) group before
the UPDATE SET name = LOWER(name).
2026-06-08 10:17:00 +02:00
Pontoporeia f4cb06656e Improve .gitignore 2026-06-08 10:14:17 +02:00
Pontoporeia 2bb520bb8c Fix: anchor vendor/ gitignore to root so app/public/assets/js/vendor/ is tracked (htmx, OverType, FilePond) 2026-06-08 10:12:39 +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 a251aeb500 Fix: bootstrap.php autoload path detects vendor location (same dir vs parent dir) for flat server layout 2026-05-31 17:46:02 +02:00
Pontoporeia d33a56981e Fix: deploy-deps patches classmap path (app/src/ → src/) for flat server layout before composer install 2026-05-31 17:46:02 +02:00
Pontoporeia 47405e5334 Fix: split deploy recipe into deploy-code/deploy-deps/deploy-migrate; deploy-deps always syncs composer.json+composer.lock, runs composer install only when lockfile checksum changed 2026-05-31 17:46:02 +02:00
Pontoporeia 4e409c409d Fix: add ZipArchive guard to export-files.php, add composer install step + composer.json sync to deploy recipe 2026-05-20 12:49:23 +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 a6e0aa5887 Add code coverage configuration (phpunit.xml source filter), baseline coverage report (21.27% lines), gitignore coverage/ and .phpunit.result.cache; remove deprecated setAccessible() calls 2026-05-20 02:08:42 +02:00
Pontoporeia a047062d87 Phase 4 cleanup: migrate old tests to PHPUnit, add ErrorHandler/PureLogic/SearchController tests, remove app/tests/, update justfile test target 2026-05-20 01:55:58 +02:00
Pontoporeia 93625d09b5 Add integration tests (Phase 2: DatabaseExtended, ShareLinkExtended, RateLimitExtended) and controller validation tests (Phase 3: ThesisCreate, ThesisEdit, AutofocusField) 2026-05-20 01:51:41 +02:00
Pontoporeia 7a4d0fafb2 Add PHPUnit setup (Phase 0) and pure-logic tests (Phase 1): Crypto, EmailObfuscator, SystemController helpers, StudentEmail, TfeController OG tags 2026-05-20 01:35:56 +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 4683ba4116 Add composer.json with league/commonmark, guzzlehttp/guzzle, phpmailer/phpmailer; wire autoloader into bootstrap; document de-librairisation strategy and PHP extension setup 2026-05-20 00:53:42 +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 2e75a3b35c Fix beforeunload dialog appearing on edit.php when no changes made 2026-05-19 23:59:07 +02:00
Pontoporeia 42222abe7c Récapitulatif admin: sections → fieldsets, fichiers en table, marges + pas de thumbnails 2026-05-19 23:58:51 +02:00
Pontoporeia defc919cd0 cleanup modal: list stale files to remove; storage restructure: documents/ → {objet}/ 2026-05-19 23:58:51 +02:00
Pontoporeia c6199525f9 add sticky thead to index, langues, and mots-clés tables 2026-05-19 23:58:51 +02:00
Pontoporeia bcf3140aa2 edit submission: redirect to recapitulatif instead of edit 2026-05-19 23:58:51 +02:00
Pontoporeia 4da317de0a deploy: remove .env from generic file perm check, remove router.php check (dev-only file) 2026-05-19 23:58:51 +02:00
Pontoporeia 5bbf633295 Contenus: add Mots-clés fieldset mirroring Langues, keep dedicated page button as backup, add Annuler cancel button to both bulk action bars, limit both table wraps to max-height:50vh with overflow scroll 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
Pontoporeia b484943128 Unnest header.css (native CSS nesting silently broken in browsers without support) 2026-05-19 23:58:51 +02:00
Pontoporeia 2cb8d71fe9 Fix dialog margins, add admin-dialog__body/styles, give trash page horizontal margins 2026-05-19 23:58:51 +02:00
Pontoporeia d619d2f116 Update TODO after CSS refactoring fixes 2026-05-19 22:34:57 +02:00
Pontoporeia 7cf020c7bd Refactor CSS architecture per css-methodology-spec.md
Split CSS into named layers: reset → colors → typography → base →
components → utilities. Each component has one unique root class in
its own file. No cross-component overrides.

New files:
- reset.css (modern-normalize base — matches project's prior reset)
- colors.css (all colour variables)
- typography.css (font faces, size/space scale, font-family vars)
- base.css (≤ 5 site-wide rules: layout, headings)
- utilities.css (sr-only, skip-link, reduced-motion)
- style.css (root @import file loading all layers)
- components/{links,focus,forms,tables,dialog,details,media,
  buttons,badges,toasts,pagination,header,search}.css

Existing files:
- variables.css → backward-compat wrapper (imports colors + typography)
- common.css → backward-compat wrapper (imports style.css)
- Page files (admin, public, form, tfe, apropos, repertoire, system,
  file-access) → removed redundant @import url(./variables.css)
- head.php → loads style.css instead of modern-normalize + common.css
- partage pages → load style.css

Fixes vs initial refactoring:
- reset.css: use modern-normalize base (not Tailwind Preflight) to
  avoid border/list/heading regressions from aggressive defaults
- components/search.css: restore !important flags on input styles
  (needed to override forms.css base input selectors)
- acces.php: add toast feedback on password copy button

Cleaned up duplicate status-badge/toast definitions from admin.css
(now live in components/badges.css and components/toast.css).
2026-05-19 16:55:32 +02:00
Pontoporeia 7c30d1c55d Fix relink: close modal + HTMX refresh for immediate pool update
- 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.
2026-05-19 01:32:34 +02:00
Pontoporeia b77bc486e5 Fix relink: FilePond addFile API, yellow border, limbo type + await
- 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.
2026-05-19 01:13:17 +02:00
Pontoporeia ae9a8a62c0 deploy: exclude storage/{tmp,documents,theses,triage,backups,logs} from rsync + fix .env perm check to 640 2026-05-19 00:33:28 +02:00
Pontoporeia 41eebf8a02 cleanup: squash commit history from 177 to 98 commits, resolve acces.php conflict markers 2026-05-19 00:09:15 +02:00
Pontoporeia 27e6abc7e4 feat: file browser + relink for orphaned files + htmx fix + header cleanup + fix relinked FilePond integration + resolve acces.php conflict markers 2026-05-19 00:08:06 +02:00
Pontoporeia 79eddf5d5a feat: fix file deletion on save + trash policy + documents/ prefix + relink browser
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
2026-05-19 00:08:06 +02:00
Pontoporeia 6f7a02244f maintenance: allow /partage through gate, fix fragment routing, add visibility table in admin
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.
2026-05-19 00:08:06 +02:00
Pontoporeia da153fc604 Refactor HTMX fragment architecture: DRY split into auth endpoints + shared templates
- Created templates/partials/form/_licence.php (shared HTML, no auth logic)
- Created templates/partials/form/_format-website.php (shared HTML, no auth logic)
- Created src/FragmentRenderer.php helper for clean fragment rendering
- Created public/{admin,partage}/fragments/ subdirectories
- Created thin fragment endpoint files: auth guard + data fetch + render template
- Updated all hx-post references in templates to new fragments/ paths
- Updated partage/index.php routing for new fragments subdirectory
- Kept old fragment files as thin delegates for backward compat
- Updated nginx config: added PHP handler in /partage/ location block
2026-05-19 00:08:06 +02:00
Pontoporeia 2632730fa0 .gitignore ignore rate_limit and theses and logs.
Done. The .gitignore now ignores all files in app/storage/cache/rate_limit/* and app/storage/theses/* while   
 preserving their .gitkeep files via ! negation rules.
2026-05-19 00:08:06 +02:00
Pontoporeia 2f4ac22bcb Fix Interdit Info text 2026-05-19 00:08:06 +02:00
Pontoporeia fdc301c20d fix: require Crypto.php in ShareLink.php 2026-05-19 00:08:06 +02:00
Pontoporeia 9152b120e8 feat: mandatory auto-generated passwords for share links + admin password copy/regeneration + password gate rate limiting 2026-05-19 00:08:06 +02:00
Pontoporeia 8bb0b3a1f2 refactor: unify FilePond edit previews + clean upload UI and shared fragments
* Move shared `fichiers-fragment.php` from `partage/` to `templates/partials/form/`
  and update all include/require references
* `.gitignore`: exclude SQLite WAL/SHM journal files
* FilePond UI:

  * change uploaded file block border state from yellow to green
  * restyle image previews to use site light-theme colors
* Edit mode:

  * remove custom existing-file preview list implementation
  * preload existing files directly into FilePond pools
  * include `cover` and `note_intention` assets in FilePond-managed state
* Remove obsolete upload progress bar UI and related JS includes
* Remove deprecated `Écriture` + `Image` format types from upload flow/configuration
2026-05-19 00:08:06 +02:00
Pontoporeia 6e7c0c00e3 refactor: merge video/audio FilePond pools into TFE input
- 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
2026-05-19 00:08:06 +02:00
Pontoporeia 1ff3c70ebe fix: track vendor JS files, add 'unsafe-inline' to public CSP, gitignore filepond tmp
- Track vendor JS files (filepond, htmx, overtype) that were moved
  to app/public/assets/js/vendor/ but never tracked → missing from deploys
- Add script-src 'self' 'unsafe-inline' to main CSP header so public
  pages (jury fieldset, repertoire, partage) can use inline scripts
  and onclick handlers
- Add storage/tmp/filepond/* to .gitignore with .gitkeep, and exclude
  from deploy rsync to avoid syncing local test uploads to production
2026-05-19 00:08:06 +02:00
Pontoporeia 2e9ebfc684 filepond: implement async server-ID upload architecture with nested queue support + PeerTube integration
Replace `storeAsFile:true` with a full async FilePond round-trip pipeline using opaque server-side file IDs.

* Added 4 new PHP endpoints under `/admin/actions/filepond/`:

  * `process.php` — upload/process single file and return opaque `file_id`
  * `revert.php` — delete pending tmp uploads before form submit
  * `load.php` — stream existing files by DB ID for FilePond preload
  * `remove.php` — soft-delete `thesis_files` rows
* `process.php` improvements:

  * accept arbitrary FilePond field names instead of hardcoded `file`
  * support PHP-nested multi-file queue inputs (`queue_file[tfe][]`)
  * explicit unwrapping of nested `$_FILES` structures
  * add `audio/mp3` to audio + `peertube_audio` MIME whitelists
  * immediate upload of `peertube_*` files to PeerTube, returning `peertube:{uuid}` IDs
  * extensive `error_log()` instrumentation for request, CSRF, MIME, upload, and save stages
* `revert.php` now accepts `peertube:` IDs without local cleanup
* `ThesisFileHandler`:

  * add `handleFilePondQueueFiles()` + `handleFilePondSingleFile()`
  * process async uploads from `storage/tmp/filepond/` via opaque `file_id`
  * inline handling of `peertube:{uuid}` IDs with direct `thesis_files` insertion
  * remove obsolete deferred PeerTube queue-processing flow
* `ThesisCreateController` + `ThesisEditController`:

  * gate async path behind `filepond_mode=1`
  * preserve legacy multipart flow as fallback
* `file-upload-filepond.js`:

  * remove `storeAsFile:true`
  * add `buildServerConfig()` for async endpoint wiring
  * fix `syncOrderInput()` to use `serverId`
  * add `onprocessfile` hook
  * add `fileValidateSizeFilterItem` for per-extension size caps
  * preload existing uploads via `data-existing-files` + `server.load`
  * replace static `INPUT_ID_TO_TYPE` map with `data-queue-type`
  * add extensive `console.log()` debugging across upload pipeline stages
* `upload-progress.js`:

  * block form submission while uploads are pending
  * update `collectFileNames()` to read processed FilePond items
* Templates/layout:

  * add `data-queue-type`
  * add `data-existing-files`
  * add global CSRF meta tag outside admin-only context
  * add `filepond_mode` hidden input
  * add CSRF token/meta support for partage pages
  * move website URL field below file upload block
* `.gitignore`: exclude `storage/tmp/` from version control
2026-05-19 00:08:06 +02:00
Pontoporeia b56d073210 refactor: extract inline JS into app/ modules, remove dead overtype-webcomponent
- Remove overtype-webcomponent.min.js (zero references)
- Extract copyLogContent + fallbackCopy + HTMX tab-updater → app/admin-logs.js
  (removes duplicate from both system.php and parametres.php)
- Extract copyUrl → app/clipboard.js (shared by acces.php)
- Extract tag/language pill-search logic → app/pill-search.js
  Generalized with data-pill-search attributes, auto-inits via
  DOMContentLoaded + htmx:afterSwap
- Extract access-request form handler → app/access-request.js
  (was inline in templates/public/tfe.php)

Files created: admin-logs.js, clipboard.js, pill-search.js, access-request.js
Files modified: 9 templates/controllers to drop inline scripts and
  reference external JS files
2026-05-19 00:08:06 +02:00
Pontoporeia 04094d802d fix: harden security based on pentest scan findings
- Add Content-Security-Policy to main nginx server block (was only on /admin/)
- Add Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy headers
- Add includeSubDomains to HSTS header
- Set HttpOnly, Secure, SameSite=Lax session cookie params on public pages
  (AdminAuth already hardens the /admin session with SameSite=Strict)
- Update xamxam.conf.reference and SECURITY_HEADERS.md to match
2026-05-19 00:08:06 +02:00
Pontoporeia 4717b4d67e feat: add passive pentest scanner script with PEP 723 uv metadata 2026-05-19 00:08:06 +02:00
Pontoporeia df12af8423 fix: validation error messages hidden by generic fallback in ErrorHandler::userMessage
ErrorHandler::userMessage only handled RuntimeException, but all validation
throws in ThesisCreateController and ThesisEditController use plain Exception.
This caused user-friendly messages like 'Le champ Nom/Prénom/Pseudo est requis'
to fall through to the 'Une erreur inattendue est survenue…' generic message.

Fix: add Exception check (after PDOException, since PDOException extends it)
so all validation exceptions pass their message through.
2026-05-19 00:08:06 +02:00
Pontoporeia c3f6e8a033 fix: upload progress bar not visible — collectFileNames checks FilePond instances; remove admin auth from progress poll endpoint 2026-05-19 00:08:06 +02:00
Pontoporeia d873a7f09e fix: add upload-progress.js to partage form (progress bar was missing on public submissions) 2026-05-19 00:08:06 +02:00
Pontoporeia e5e76cfb70 add migration 033 to deduplicate format_types, fix 019 to use INSERT OR IGNORE 2026-05-19 00:08:06 +02:00
Pontoporeia 3ae22cd427 add system dependency checks (php-curl, sqlite3) to deploy-server.sh step 0 2026-05-19 00:08:06 +02:00
Pontoporeia 31ccbd195b add syntaqlite SQL validation to migrate.sh before applying schema.sql 2026-05-19 00:08:06 +02:00
Pontoporeia cb6394e119 add incremental migration runner to deploy recipe — execute whole SQL files (not semicolon-split), catch 'no such column' for idempotent re-runs, merge into migrate.sh 2026-05-19 00:08:06 +02:00
Pontoporeia c1960d224b fix deploy: multiple deploy recipe fixes — upload xamxam.conf before deploy-server.sh, sudo rsync for chown, migrate.sh via sqlite3, chmod WAL/SHM sidecar files, deploy-verify-permissions awk fix, .env sudo perms 2026-05-19 00:08:06 +02:00
Pontoporeia 206a6427e7 style: unify recap+edit file figure styling — two-column grid dl, vertical spacing, admin-back-btn sizing, standardise file display and delete-to-trash SVG icons 2026-05-19 00:08:06 +02:00
Pontoporeia 230555a4c4 exhaustive recap: all fields, figures for files, PeerTube ID display, same in edit form 2026-05-19 00:08:06 +02:00
Pontoporeia 8bf95f4e14 feat: refactor licence fragment — Libre→CC2r+licence, Interne→opt-in want_license, Interdit→none, add details/summary to license UI 2026-05-19 00:08:06 +02:00
Pontoporeia 927ee2fe2a feat: upload progress bar — fieldset layout, accent colors, file name display, completion animation, 800ms redirect delay; decorelate formats from fichiers; server-side poll via token; bump PeerTube embed audio player 2026-05-19 00:08:06 +02:00
Pontoporeia cdec3e96a6 fix PeerTube upload: final working solution — simple multipart POST with CURLFile; iterated through Google-resumable PATCH protocol debugging (HTTP version negotiation, chunk body encoding, off-by-one fixes) before settling on simpler POST approach 2026-05-19 00:08:06 +02:00
Pontoporeia 1b0451581d refactor: move Restrictions d'accès aux fichiers from contenus.php to acces.php, cleanup section 2026-05-19 00:08:06 +02:00
Pontoporeia d000f9e1d4 cleanup: merge SMTP fields into single fieldset, rename to Emails
- Renamed section from 'Relay SMTP' to 'Emails'
- Merged 'Expéditeur par défaut' fieldset into the main SMTP grid
- Removed separate 'from_email' field: now uses username as from_email
- Changed username label from 'Nom d\'utilisateur' to 'Adresse e-mail'
  with placeholder xamxam@erg.be and type=email
- from_name and notify_email now inline in the main grid after password
2026-05-19 00:08:06 +02:00
Pontoporeia 83a5a508ea feat: PeerTube integration — alternate audio/video labels, FilePond pools, shared SMTP credentials, channel by name, test button, resumable upload, embed improvements, fix alt labels/curl_close/deprecation 2026-05-19 00:08:06 +02:00
Pontoporeia 28ef35dce5 fix: make schema.sql fully idempotent — add IF NOT EXISTS to all CREATE INDEX, CREATE TRIGGER, and CREATE VIEW statements 2026-05-19 00:08:06 +02:00
Pontoporeia 973444bdbb feat(backup): deploy cron-based SQLite backups to production
- Create deploy/xamxam-backup.cron with hourly (30d) and daily (90d) jobs
- Add just recipes for deploying backup infrastructure:
  - deploy-backup-script: upload backup-sqlite.sh to /usr/local/bin
  - deploy-backup-cron: install cron.d file, create /var/backups/xamxam + log
  - deploy-backup: one-shot convenience (script + cron)
  - deploy-check-backup-log: tail the backup log
  - deploy-list-backups: ls remote backup directory
  - trigger-backup: manually invoke backup on server
  - test-restore: scp, gunzip, verify a remote snapshot
- Add reminder to run deploy-backup after first deploy
- Replace 'Contenu (Markdown)' label with 'Syntax Markdown' link (cheatsheet)
2026-05-19 00:08:06 +02:00
Pontoporeia be50ac5eb0 fix(production): fix multiple remote server errors from nginx logs
- Fix 413 Request Entity Too Large: bump client_max_body_size to 256M,
  PHP post_max_size/upload_max_filesize to 256M, fastcgi timeouts to 300s
- Fix missing v_smtp_active view: add IF NOT EXISTS to all CREATE VIEW
  statements in schema.sql for idempotent migrates
- Fix bars.svg 404: create animated SVG spinner in app/public/assets/img/
- Fix nginx rate limiting: increase admin zone from 60r/m (1 r/s) to
  300r/m (5 r/s) with burst=30 to handle ~11 concurrent HTMX fragment
  GETs on contenus.php page load
- Add deploy-nginx recipe to justfile for uploading nginx config to server
- Database readonly issue mitigated by existing --chown + deploy-server.sh
  permissions fix
- Add comprehensive PHP/JS debugging logs for settings checkboxes:
  per-field raw POST values in error_log, console.log on htmx:beforeSend,
  htmx:sendError, htmx:afterRequest, toast lifecycle
- Fix toast auto-remove script: use getElementById with unique ID instead
  of querySelector which could remove wrong toast on rapid clicks
2026-05-19 00:08:06 +02:00
Pontoporeia 43064ccbd7 feat(admin): add htmx toast feedback for settings checkboxes in contenus.php
- Replace hx-swap="none" with hx-target on response divs inside each of the
  three fieldsets (Restrictions d'accès, Degré d'ouverture, Types de travaux)
- Add hxToastSuccess / hxToastError helpers in settings.php that return HTML
  toast fragments with self-referencing auto-dismiss after 3s
- Each response div has aria-live="polite" for accessibility
- Add comprehensive PHP/JS debugging logs:
  - settings.php logs raw POST values per field before resolving to 0/1
  - checkboxes have hx-on::before-request and hx-on::after-request console.log
  - global htmx:beforeSend and htmx:sendError listeners in admin footer
  - toast lifecycle logged (creation + removal) for traceability
- Fix toast auto-remove: use getElementById with random unique ID instead
  of querySelector which could remove wrong toast on rapid clicks
- Follows the Django+HTMX ajax checkbox pattern from the reference tutorial

feat(admin): add htmx toast feedback for settings checkboxes in contenus.php

- Replace hx-swap="none" with hx-target on response divs inside each of the
  three fieldsets (Restrictions d'accès, Degré d'ouverture, Types de travaux)
- Add hxToastSuccess / hxToastError helpers in settings.php that return HTML
  toast fragments with self-referencing auto-dismiss after 3s
- Each response div has aria-live="polite" for accessibility
- Add comprehensive PHP/JS debugging logs:
  - settings.php logs raw POST values per field before resolving to 0/1
  - checkboxes have hx-on::before-request and hx-on::after-request console.log
  - global htmx:beforeSend and htmx:sendError listeners in admin footer
  - toast lifecycle logged (creation + removal) for traceability
- Fix toast auto-remove: use getElementById with random unique ID instead
  of querySelector which could remove wrong toast on rapid clicks
- Fix checkbox unresponsive after toggles: move hidden value="0" inputs
  outside <label> to prevent HTML label double-activation
- Follows the Django+HTMX ajax checkbox pattern from the reference tutorial

feat(admin): add htmx toast feedback for settings checkboxes in contenus.php

- Replace hx-swap="none" with hx-target on response divs inside each of the
  three fieldsets (Restrictions d'accès, Degré d'ouverture, Types de travaux)
- Add hxToastSuccess / hxToastError helpers in settings.php that return HTML
  toast fragments with self-referencing auto-dismiss after 3s
- Each response div has aria-live="polite" for accessibility
- Add comprehensive PHP/JS debugging logs:
  - settings.php logs raw POST values per field before resolving to 0/1
  - checkboxes have hx-on::before-request and hx-on::after-request console.log
  - global htmx:beforeSend and htmx:sendError listeners in admin footer
  - toast lifecycle logged (creation + removal) for traceability
- Fix toast auto-remove: use getElementById with random unique ID instead
  of querySelector which could remove wrong toast on rapid clicks
- Fix checkbox unresponsive after toggles: remove hidden value="0" inputs entirely; unchecked checkboxes are simply absent from POST and server treats missing key as 0
  outside <label> to prevent HTML label double-activation
- Follows the Django+HTMX ajax checkbox pattern from the reference tutorial
2026-05-19 00:08:06 +02:00