Commit Graph

301 Commits

Author SHA1 Message Date
Pontoporeia
77bfd2f8e3 Extract status-badge.php partial; replace inline badge markup in index.php and account.php
Add templates/partials/status-badge.php — a single reusable partial that
renders the <span class="status-badge …"> element for three badge types:

  'publish'  — Publié / En attente derived from a boolean is_published value
  'access'   — access-type label (Libre / Interne / Interdit) with slug-based
               CSS modifier class and appropriate symbol (○ ◑ ●)
  'ok'       — generic green/yellow boolean badge with caller-supplied labels
               (used for 'Active'/'Non configurée' and 'Présent'/'Absent' in
               account.php)

All three variants emit aria-label with a context prefix and wrap the
decorative symbol in aria-hidden="true" — behaviour identical to the
inline code they replace.

Callers set $badgeType + $badgeValue (+ optional $badgeOkLabel /
$badgeWarnLabel / $badgeContext) before the include; the partial unsets
all working variables after rendering so they do not bleed into the
including scope.

Files changed:
  templates/partials/status-badge.php  — new partial
  public/admin/index.php               — table status column now uses partial
                                         (removes 15 lines of inline if/else/php)
  public/admin/account.php             — two credential status rows now use partial
                                         (removes 8 lines of inline if/else)
2026-04-02 12:50:46 +02:00
Pontoporeia
2143869b1e Add admin form field partials and apply to add/edit forms
Four reusable PHP partials extracted to templates/partials/form/:

- text-field.php  — single-line input (text/number/url); wraps input+hint in div,
                    skips the inner wrapper when no hint is present. Supports $type,
                    $placeholder, $required, $attrs, $hint, $id overrides.
- select-field.php — <select> with leading empty option; matches $selected against
                    option id OR option name string (handles view-sourced data where
                    orientation/ap/finality come back as name strings, not FK ids).
- checkbox-list.php — checkbox group (languages, formats); renders .admin-checkbox-list
                    with typed-string comparison so int ids from DB match string values.
- file-field.php  — file input with accept/multiple/hint; appends [] to name when
                    $multiple is true.

Both add.php and edit.php rewritten to use the partials:
- ~15 repeated text-field divs collapsed to single-line include calls
- ~6 repeated select divs collapsed to single-line include calls
- 4 checkbox-list blocks collapsed to 2 calls each
- 3 file input blocks collapsed to single-line include calls
- Textarea fields (synopsis, context_note) kept inline — no partial for <textarea>
- Banner preview block in edit.php kept inline — conditional UI not generalised

Line count: add.php 251→93 (-158), edit.php 289→171 (-118)
2026-04-02 12:48:04 +02:00
Pontoporeia
c8a3cc0ff2 css: replace admin-form-row/admin-label/admin-input/select/textarea classes with semantic selectors
Remove five presentational classes from admin forms and replace with
structural CSS selectors scoped to .admin-form:

- .admin-form-row  → .admin-form > div:not(.admin-submit-wrap)
  Grid layout (260px label col + 1fr input col) applied directly to div
  children of the form; submit-wrap div excluded via :not().

- .admin-label     → .admin-form > div:not(.admin-submit-wrap) > label
  Scoped to the direct label child of each form row div; does not bleed
  into nested checkbox labels inside .admin-checkbox-list.

- .admin-input / .admin-select / .admin-textarea
  → .admin-form input:not([type=checkbox|radio|file|hidden|submit])
  → .admin-form select
  → .admin-form textarea
  Also extended to .admin-inline-form input/select (tags page) so the
  tags table inputs retain identical base styling and focus colour.

Templates updated: add.php, edit.php, login.php, account.php,
pages-edit.php, import.php, tags.php,
templates/partials/form/jury-fieldset.php — all class= attributes for
the five removed classes stripped.

import.php: added 'admin-form' class alongside 'admin-import-area' so
its single file-input row gets the grid row treatment; submit div was
already using admin-submit-wrap so it is correctly excluded.

No visual change — selectors target the same elements as before.
2026-04-02 12:42:49 +02:00
Pontoporeia
e9e012376d Replace .admin-alert BEM classes with semantic role/data-type attributes
- admin.css: replace .admin-alert / .admin-alert--error / .admin-alert--success
  selectors with [role="alert"][data-type="error"] and [role="status"][data-type="success"]
- All 10 admin templates updated: <div class="admin-alert admin-alert--{type}">
  becomes <p role="alert|status" data-type="error|success"> (or <div> for the
  import.php multi-item list that contains a <ul>)
- flash-messages.php partial updated to match
- WCAG benefit: role="alert" is an ARIA live region — errors are announced
  immediately by screen readers without focus movement (fixes WCAG 3.3.1, 4.1.2)
- role="status" (polite live region) used for success messages — announced
  without interrupting the user
- Removes two BEM modifier classes; CSS now targets element semantics directly
2026-04-02 12:35:23 +02:00
Pontoporeia
10b07393fe Extract jury-fieldset.php partial; deduplicate jury section from add.php and edit.php
The jury composition fieldset (président·e, promoteur·ice + external checkbox, dynamic
lecteur·ices list with JS add/remove) was copy-pasted verbatim between the two longest
admin forms.

- Created templates/partials/form/jury-fieldset.php
  - Consumes $juryPresident, $juryPromoteur, $juryPromoteurExt, $juryLecteurs[]
  - Handles both add-mode (falls back to old()/wasSelected() flash helpers) and
    edit-mode (pre-populates from DB-loaded variables)
  - $juryIdx initialised from max(count($juryLecteurs), 1) — correct for both modes
- add.php: 311 → 251 lines (-60); entire fieldset + <script> replaced with one require
- edit.php: 359 → 289 lines (-70); PHP variable extraction kept inline before require
2026-04-02 12:26:44 +02:00
Pontoporeia
7834d88873 Extract pagination into templates/partials/pagination.php
The pagination nav was duplicated between public/index.php and public/search.php
with structural differences: index.php used string concatenation for query params
and had first/last-page buttons (« »); search.php used http_build_query but had
only prev/next (‹ ›) and a flat <span> rather than a <ul>/<li> structure.

- Add templates/partials/pagination.php: accepts $page, $totalPages, $baseParams[]
  (any array of query params to preserve); builds URLs with http_build_query;
  renders a semantic <nav>/<ul>/<li> block with first/prev/info/next/last buttons,
  correct aria-disabled + tabindex on disabled links, and aria-label on each button.
  Returns immediately (no output) when $totalPages <= 1.

- Replace inline pagination block in index.php with:
    $baseParams = array_filter(['year' => $year]);
    include pagination.php

- Replace inline pagination block in search.php with:
    $baseParams = array_diff_key($_GET, ['page' => '']);
    include pagination.php
  This also upgrades search.php to the full first/last button set it was missing.

Both callers verified with php -l. No functional change to existing behaviour.
2026-04-02 12:20:31 +02:00
Pontoporeia
0ab08f3aa0 admin.css: replace .admin-main, .admin-page-title, .admin-table, .admin-fieldset with semantic selectors
Replace four presentational class names in admin.css with structural selectors
that target native HTML elements already present in every admin template:

  .admin-main           → .admin-body main
  .admin-page-title     → .admin-body main > h1
  .admin-table          → .admin-body table
  .admin-fieldset       → .admin-body fieldset
  .admin-fieldset-legend → .admin-body legend

Also migrate the .admin-main > section / h2 / dl / dt / dd block to
.admin-body main > section so the thanks-page section styles survive.

Add .admin-body main > table { margin-top: 1.5rem } to absorb the inline
style="margin-top:1.5rem" that was on tags.php's <table class="admin-table">.

All 10 affected admin templates updated (add, edit, account, index, import,
pages, pages-edit, tags, system, thanks) — class attributes removed where
the element alone is now the selector.  Zero visual changes.
2026-04-02 12:16:59 +02:00
Pontoporeia
cb1ced535b Replace .admin-hint / .admin-field-hint with .admin-body form small
- admin.css: remove .admin-hint and .admin-field-hint class rules; add
  .admin-body form small with the same font-size/color/margin properties
  plus display:block so it stacks below sibling inputs; stub comment left
  where .admin-field-hint was to document the change
- add.php: 5× <p class="admin-hint"> → <small>
- edit.php: 3× <p class="admin-hint"> → <small>
- import.php: <div class="admin-hint"> → <small> (block hint below CSV input)
- pages-edit.php: class="admin-hint" removed from already-correct <small>
- account.php: <p class="admin-field-hint"> → <small>

Hint text is now styled purely via the semantic element selector; no class
required on any hint element in admin templates.
2026-04-01 17:31:11 +02:00
Pontoporeia
f208423e8d Extract system.php inline <style> and <script> to system.css / $extraJsInline
- Create public/assets/css/system.css with all 280 lines of CSS that were
  inline in system.php: tab bar, status cards, PHP info grid, disk bar,
  log viewer, nginx config viewer, and syntax-highlight classes.
- Disk bar dynamic values (width %, colour) moved from PHP-interpolated CSS
  rules to CSS custom properties (--disk-pct, --disk-color) set on the
  element via an inline style attribute; static .disk-bar rule in system.css
  consumes them via var().
- system.php JS block (tab-select auto-nav + copy-to-clipboard) moved to
  $extraJsInline heredoc; footer.php emits it before </body> — keeps it
  out of the document <head> and removes the bare <script> after </main>.
- system.php now sets $extraCss = ['/assets/css/system.css'] so head.php
  emits a proper <link> in <head>, consistent with all other admin pages.
- No behaviour change; system.php is now zero inline CSS/JS.
2026-04-01 17:24:36 +02:00
Pontoporeia
cd58bc13e4 css: replace presentational class selectors with semantic element selectors
Replace 6 CSS class selectors across tfe.css, main.css, and search.css with
semantic element-based selectors, removing the corresponding classes from the
HTML templates entirely.

tfe.css:
- .tfe-meta-list → article dl / article dl > div / article dl dt / article dl dd
- .tfe-media-block → aside figure (+ img, video, embed children)
- .tfe-file-caption → aside figcaption

main.css:
- .card__media → .home-body figure (+ img/video children and hover/motion rules)
- .card__caption → .home-body li > a > p

search.css:
- .repertoire-col > h2 → .repertoire-index section > h2

Template changes:
- tfe.php: removed class= from <dl>, <figure>, and <figcaption>
- index.php: removed class= from <figure> and <p class=card__caption>;
  stripped orphaned card__media from the gradient <div> (only --gradient needed)

No visual change — selectors match the same elements as before since the
semantic HTML was already in place from prior refactoring work.
2026-04-01 17:08:12 +02:00
Pontoporeia
77576e966c Remove inline styles from admin templates; extract to admin.css utility classes
- login.php: removed style= on .admin-form-row and .admin-label (already covered
  by .admin-login-box scoped rules); extracted submit-wrap spacing and full-width
  button to .admin-login-box .admin-submit-wrap and .admin-login-box .admin-btn
- account.php: style="margin-top:3rem" on danger-zone heading moved to
  .admin-section-title--danger modifier; <span style="color:..."> replaced with
  <small> element styled via .admin-danger-zone__description small
- add.php / edit.php / pages-edit.php: all style="align-items:start" removed from
  .admin-form-row (redundant — already the CSS default at line 116 of admin.css);
  banner preview inline styles extracted to .admin-banner-preview / .admin-banner-preview img;
  add-jury button margin extracted to .admin-add-jury-btn; cancel links use .admin-cancel-link

Zero inline style= attributes remain in login, account, add, edit, pages-edit.
2026-04-01 16:55:29 +02:00
Pontoporeia
573747303f admin: semantic HTML improvements — dl stats, section cards, th scope
- admin/index.php: replace <div class="admin-stats"> with <dl>; inner
  <div class="admin-stat__number"> → <dd>, <div class="admin-stat__label"> → <dt>;
  use CSS order to keep number visually first; add scope="col" to all 9 <th> cells

- admin/thanks.php: replace all four <div class="admin-thesis-info"> wrappers
  with <section> elements; remove the class entirely; add scope="col" to
  the files table <th> cells

- admin/tags.php: add scope="col" to all 3 <th> cells

- admin/pages.php: add scope="col" to all 4 <th> cells

- admin.css: rename .admin-thesis-info selectors to .admin-main > section
  (element + context selector — no class needed); add display:flex +
  flex-direction:column to .admin-stat so CSS order property works correctly

Addresses TODO items: section X (admin-stats dl, th scope), XI (tags th scope),
XII (admin-thesis-info → section), XIII (pages.php th scope)
2026-04-01 16:50:53 +02:00
Pontoporeia
8e36f98139 Move RateLimit cache dir from src/cache/ to storage/cache/rate_limit/
The default cache directory for the file-based rate limiter was
src/cache/rate_limit/, placing transient JSON files inside the source tree.
This meant:
- The directory was deployed via rsync on every deploy (wasted I/O)
- .gitignore had to track a src/-internal path
- Developers running tests could leave stale cache state in the source tree

Changes:
- src/RateLimit.php: default $cacheDir changed from __DIR__.'/cache/rate_limit'
  to dirname(__DIR__).'/storage/cache/rate_limit'; dirname(__DIR__) resolves to
  the project root regardless of how the file is loaded (with or without bootstrap)
- .gitignore: replaced 'src/cache/rate_limit/' with 'storage/cache/' (broader,
  covers any future cache subdirs under storage/)
- storage/cache/.gitkeep: added so the directory is tracked in VCS and created
  on fresh clones/deploys, but its contents are ignored
- justfile: added '--exclude storage/cache/*' to the deploy rsync recipe so
  rate-limit state is never transferred to the server
- src/cache/: removed (no longer needed)

All RateLimit unit tests pass.
2026-04-01 16:44:07 +02:00
Pontoporeia
9108c4069d restore TODO.md: merge current active tasks with full historical TODO recovered from kkmmrrrkkyrs 2026-04-01 15:58:42 +02:00
Pontoporeia
a5ee9b162f Replace site-search BEM classes with semantic header form[role="search"] selectors
CSS: .site-search → header form[role="search"],
     .site-search__icon → header form[role="search"] svg,
     .site-search__input → header form[role="search"] input,
     .site-search__input::placeholder → header form[role="search"] input::placeholder

HTML: Removed class="site-search", class="site-search__icon", and
class="site-search__input" from header.php and search-bar.php.
The form already uses role="search" and contains a single svg + input,
so the semantic selectors are unambiguous.
2026-04-01 15:55:12 +02:00
Pontoporeia
92a07d0b99 TODO: add targeted tasks for template simplification, PHP partials/components, and system page caching 2026-04-01 15:55:12 +02:00
Pontoporeia
eb67e6d499 Add src/App.php foundation class and flash-messages partial
Create the central App helper that eliminates ~170 lines of duplicated
bootstrap/auth/CSRF preamble across 24 page and action handler files.

src/App.php provides:
- boot(): loads Database + ensures CSRF token (public pages)
- adminGuard(): requires AdminAuth login + boot (admin pages)
- verifyCsrf() / rotateCsrf(): centralised CSRF lifecycle
- flash() / consumeFlash(): unified flash messages with legacy key drain
  (error, success, admin_error, admin_success, edit_error, edit_success,
  form_error all consumed transparently for incremental migration)
- redirect(): flash + Location header + exit in one call
- render(): head → header → content → footer pipeline with auto admin
  footer selection

App.php is auto-loaded from config/bootstrap.php so all existing pages
get the class for free without any changes.

templates/partials/flash-messages.php uses App::consumeFlash() to replace
the 5+ copy-pasted flash blocks across admin templates.

All existing tests pass. No existing page files modified — this is a
non-breaking addition that enables incremental controller extraction.
2026-04-01 15:55:12 +02:00
Pontoporeia
7aace2a551 Add refactoring recommendations for controller/template/routing separation 2026-04-01 15:55:12 +02:00
Pontoporeia
8976e52d10 Add PHP vs Flask architecture analysis 2026-04-01 15:55:12 +02:00
Pontoporeia
780b1b2a13 merge head/nav templates into unified head.php + header.php; semantic CSS for nav 2026-04-01 15:55:12 +02:00
Pontoporeia
4ff959a72d fix template consolidation: admin/head.php wraps public/head.php, footer.php wired to all public pages, remove duplicate font-family and body reset 2026-04-01 15:55:12 +02:00
Pontoporeia
3a42838cec consolidate admin/public templates: common.css base in admin, nav partial, remove duplicate CSS 2026-04-01 15:55:12 +02:00
Pontoporeia
f3f1e0e5fc Replace unicode left arrow with SVG icon in admin nav logo 2026-04-01 15:55:12 +02:00
Pontoporeia
a88e5562f8 fix(config): auto-route test.db locally, posterg.db on production
- config.php: getDatabasePath() detects php built-in CLI server
  (php_sapi_name() === 'cli-server') and routes to test.db; all
  other SAPIs (nginx/fpm) get posterg.db. DB_ENV env-var still
  overrides either way.

- migrate.sh: auto-initialise the target DB from storage/schema.sql
  when the file is absent or has no tables yet. Existing DBs with
  data are left completely untouched (table_count check, no re-run
  of schema on populated DB). Idempotent: safe to run repeatedly.

- justfile: serve still calls migrate (which now handles init too),
  no DB_ENV prefix needed since sapi detection handles routing.
2026-04-01 15:55:12 +02:00
Pontoporeia
877e322568 fix(import): set is_published=1 and map access_type_id on CSV import
Imported theses were invisible on the public site because:
1. is_published defaulted to 0 (schema default) — the INSERT never
   set it, so all imported rows stayed unpublished and were filtered
   out by v_theses_public (WHERE is_published = 1) and every public
   DB method.
2. The access column (CSV col 16 'Autorisation') was read into $access
   but never written to access_type_id — silently dropped.

Fix: INSERT now includes is_published = 1 and access_type_id (resolved
from access_types.name via ucfirst/strtolower normalisation, defaulting
to 1/Libre when the CSV cell is empty or unrecognised).
2026-04-01 15:55:12 +02:00
Pontoporeia
72d48c49c3 feat(db): auto-migrate both DBs on serve via scripts/migrate.sh 2026-04-01 15:55:12 +02:00
Pontoporeia
af06e09caa fix(import): skip rows with duplicate identifier instead of crashing 2026-04-01 15:55:12 +02:00
Pontoporeia
e5d0598208 fix: correct require_once path depth in admin action files 2026-04-01 15:55:12 +02:00
Pontoporeia
94f3fb6736 feat(admin): nav logo links back to public site; all nav links right-aligned
templates/admin/head.php:
  - admin-nav__logo now href="/" with target="_blank" rel="noopener noreferrer"
  - Left arrow prefix (← via &#8592;, aria-hidden) signals leaving admin
  - sr-only suffix "(site public, nouvel onglet)" for screen readers

public/admin/login.php:
  - Same treatment on the standalone login nav (was a bare <span>)

public/assets/css/admin.css:
  - admin-nav__list: flex:1 removed; margin-left:auto added
    → entire link list now right-justified inside the nav bar,
      mirroring the layout of the public site header
  - .admin-nav__logout { margin-left:auto } removed (no longer needed;
    logout is just the last item in a right-aligned list)
2026-04-01 15:55:12 +02:00
Pontoporeia
77cc3caa0a fix(a11y): status badges no longer colour-only; fix aria on ✕ buttons (WCAG 1.4.1, 2.5.3)
admin/index.php — status badges (WCAG 1.4.1 Use of Colour):
  - Published badge: prefix ● symbol (aria-hidden) + aria-label="Statut : Publié"
  - Pending badge:   prefix ◌ symbol (aria-hidden) + aria-label="Statut : En attente"
  - Access badges (Libre/Interne/Interdit): prefix ○/◑/● symbol per type (aria-hidden)
    + aria-label="Accès : [type]"; symbol chosen from a PHP map keyed on the slug
  Each badge now communicates its state through shape AND colour, not colour alone.

admin/index.php — ✕ Réinitialiser link (WCAG 2.5.3 / 1.1.1):
  - ✕ wrapped in <span aria-hidden="true"> so the decorative symbol is skipped by
    screen readers; accessible name remains "Réinitialiser"

admin/add.php + admin/edit.php — jury remove buttons (WCAG 2.5.3):
  - All four ✕ remove buttons (2 static template rows + 2 JS-generated innerHTML strings)
    given aria-label="Supprimer ce lecteur"; the bare ✕ Unicode character has no
    speech equivalent so the aria-label replaces rather than supplements the label
2026-04-01 15:55:12 +02:00
Pontoporeia
338782947c chore: vendor all CDN assets locally; reorganise assets into css/ and js/
All third-party assets are now self-hosted — zero external requests at runtime.

CSS (assets/css/):
  - modern-normalize.min.css  (was assets/)
  - common.css, admin.css, main.css, search.css, tfe.css, apropos.css  (was assets/)
  - easymde.min.css 2.20.0  (was cdn.jsdelivr.net)
  - font-awesome.min.css 4.7.0  (was maxcdn.bootstrapcdn.com; injected at runtime by EasyMDE)

JS (assets/js/):
  - easymde.min.js 2.20.0  (was cdn.jsdelivr.net)

Fonts (assets/fonts/fontawesome/):
  - fontawesome-webfont.{eot,woff2,woff,ttf,svg}, FontAwesome.otf 4.7.0

Path fixes:
  - common.css @font-face: ./fonts/ -> ../fonts/ (one level deeper)
  - font-awesome.min.css @font-face: ../fonts/ -> ../fonts/fontawesome/ (dedicated subdir)
  - pages-edit.php: autoDownloadFontAwesome:false added to EasyMDE init to
    suppress the runtime CDN injection that was still present inside easymde.min.js

Reference updates (all now absolute /assets/css/* or /assets/js/*):
  - templates/public/head.php: modern-normalize + common
  - templates/admin/head.php: modern-normalize + admin
  - public/admin/login.php: modern-normalize + admin (standalone head)
  - public/index.php, tfe.php, search.php, apropos.php, licence.php: extraCss paths
  - public/admin/pages-edit.php: extraCss + extraJs (font-awesome, easymde CSS/JS)

Nginx static-file location already covers .css/.js/.woff/.woff2/.ttf/.otf with
30-day cache headers — no nginx config change needed.
2026-03-31 15:44:48 +02:00
Pontoporeia
986945a347 fix(a11y): move pages-edit EasyMDE scripts to head/footer, add h1 to home, fix stale TODO items
- pages-edit.php: EasyMDE CDN JS URL moved to $extraJs (rendered by footer.php before </body>);
  inline EasyMDE init block moved to $extraJsInline, emitted by footer.php via new
  `<?php if (!empty($extraJsInline))` guard - fixes invalid <script> floating in <body> (WCAG 4.1.1)
- pages-edit.php: add <small> keyboard-trap hint below the editor textarea:
  'Appuyez sur Échap pour quitter l'éditeur au clavier.' (WCAG 2.1.2)
- templates/admin/footer.php: extend to support $extraJsInline (raw inline script string)
- index.php: add <h1 class="sr-only">Mémoires de l'ERG</h1> inside <main> so the page has
  a document heading (WCAG 2.4.6; h2 columns in search.php already had a sr-only h1)
- TODO.md: mark completed items as [x]: skip links (2.4.1), focus-visible / outline:none
  removal (2.4.7), search.php h1 + index.php h1 (2.4.6), pages-edit.php invalid HTML (4.1.1),
  EasyMDE keyboard trap hint (2.1.2)
2026-03-31 15:28:47 +02:00
Pontoporeia
59ae2151d0 semantic HTML: apropos.php and licence.php (TODO section V & VI)
apropos.php:
- Remove redundant <div class="apropos-left"> wrapper; prose div is now a direct
  grid child
- <div class="apropos-description apropos-page-content"> → <div class="prose">
  (single canonical class for Markdown-rendered content)
- <div class="apropos-right"> → <aside class="apropos-aside"> (supplementary info
  landmark; contacts and credits are secondary to the main description)
- Three bare <div> wrappers inside the aside → <section> (erg link, Contacts, Crédits)
- Three <div class="apropos-contact"> entries → <address> with font-style:normal
  override; <span class="apropos-contact-name"> → <strong>;
  <span class="apropos-contact-email"> → <a href="mailto:…">
  Removes classes: apropos-left, apropos-right, apropos-contact, apropos-contact-name,
  apropos-contact-role, apropos-contact-email, apropos-description, apropos-page-content

licence.php:
- <div class="apropos-description apropos-page-content apropos-single"> →
  <div class="prose apropos-single"> (consistent with apropos.php rename)

apropos.css:
- Rename .apropos-description / .apropos-page-content → .prose; merge all prose
  content rules under the single .prose selector
- Rename .apropos-right → .apropos-aside; remove .apropos-left (empty rule)
- Replace .apropos-contact, .apropos-contact-name etc. with element selectors:
  .apropos-aside address, .apropos-aside address strong,
  .apropos-aside address span, .apropos-aside address a
- Update responsive blocks to reference .prose instead of .apropos-description
2026-03-29 17:01:53 +02:00
Pontoporeia
f2c023e19a admin nav: replace bare <a> links with <ul>/<li>, use aria-current instead of .active class
- templates/admin/head.php: all 7 nav links (+ conditional Modifier + Déconnexion)
  wrapped in <ul class="admin-nav__list">/<li>; .active class removed, replaced
  with aria-current="page" on each <a> based on $currentPage match
- Déconnexion link: removed inline style="margin-left:auto;opacity:.6;"; moved to
  new .admin-nav__logout <li> class in admin.css
- public/assets/admin.css: replaced .admin-nav__link rules with .admin-nav__list a
  selectors; added .admin-nav__list (flex list, gap 2.5rem, flex:1); added
  .admin-nav__list a[aria-current="page"] rule (border-bottom underline indicator);
  added .admin-nav__logout / .admin-nav__logout a for the push-right logout item
- Removes .admin-nav__link class entirely from the codebase (was only used in
  templates/admin/head.php and admin.css)

Fixes WCAG 2.4.6 (nav landmark content model), 1.4.1 (colour-only active indicator),
and section VIII of the semantic HTML admin audit.
2026-03-29 16:31:26 +02:00
Pontoporeia
ac872c1fe0 Semantic HTML: home page card grid — <ul>/<li>/<figure>/<nav> refactor
Replace presentational divs in index.php and main.css with elements that
carry correct semantic meaning, fixing multiple WCAG 2.1 AA issues:

index.php:
- <div class="cards-container"> → <ul class="cards-container"> (list of navigable items)
- <a class="card-link"><div class="card">…</div></a> → <li class="card"><a> (block link
  is the <a>, <li> is the container; removes the redundant .card div wrapper)
- <div class="card__media"> → <figure class="card__media"> when wrapping an <img>;
  gradient placeholder stays as <div> (presentational, aria-hidden)
- Improved alt text: "Couverture — [title] par [authors]" instead of bare title
- Removed <div class="card__info"> wrapper; caption is now a bare <p class="card__caption">
  directly inside the <a>
- <div class="filter-info"> → <p class="filter-info" role="status"> (live-region
  semantics; announces filter state to screen readers)
- ✕ symbol in clear-filter link wrapped in <span aria-hidden="true">
- Gradient placeholder div gets aria-hidden="true" (decorative; caption below carries text)
- Empty-state <p style="…"> → <li class="cards-empty"> (removes inline style)
- <div class="pagination-wrap"> → <nav class="pagination-wrap" aria-label="Pagination">
  with <ul>/<li> children; page-info <span> → <li aria-current="page">

main.css:
- .cards-container: add list-style:none; margin:0; padding:0 (reset <ul> defaults)
- Remove .card-link rule; replace with .card > a (block flex link, no separate class)
- .card__media: add margin:0 to reset <figure> default margin
- Remove .card__info rules; rename .authors to .card__caption with same styles
- Add .cards-empty rule (removes last inline style from index.php)
- .pagination-wrap: restructured for <nav>/<ul>; inner <ul> carries the flex layout
- prefers-reduced-motion: add .card__media--gradient guard

WCAG criteria addressed: 1.1.1 (alt text), 1.3.1 (info & relationships via semantic
list/figure), 2.4.1 (filter-info now live region), role="status" on filter banner.
2026-03-29 16:13:02 +02:00
Pontoporeia
c352a392a1 search.php: semantic HTML overhaul of répertoire index and results view
- Replace 4x <div class="repertoire-col"> with <section>; remove
  .repertoire-col__header class, CSS now targets section > h2
- Wrap all index link groups in <ul>/<li>; delete the four per-column
  link classes (year-index-item, cat-index-item, student-index-item,
  keyword-index-item); active state switches from .active to
  aria-current="page" on the <a>
- Add <h1 class="sr-only">Répertoire</h1> so the index view has a
  page-level heading (WCAG 2.4.6)
- Remove redundant <div class="search-results-view"> wrapper; padding
  moved to .results-grid and .search-results-header directly
- Replace <div class="results-grid"> with <ul class="results-grid">;
  each result card becomes <li><a class="result-card">
- Replace <span class="result-card__meta"> with <small> (ancillary
  metadata per HTML spec)
- Replace result-count <p> with <output role="status"> (computed value)
- Replace 3x <div class="search-filter-group"><label>…</label><select>
  with <label> directly wrapping <select> (implicit association,
  removes .search-filter-group divs); CSS updated to display:flex on
  the label itself
- Pagination wrapper changed to <nav aria-label="Pagination">;
  page-info span gets aria-current="page"
- search.css: delete .search-results-view, four index-item classes,
  .cat-index-group, .search-filter-group; consolidate years/other
  column link styles under .repertoire-col:first-child ul a and
  .repertoire-col:not(:first-child) ul a selectors; add ul reset rule
2026-03-29 16:07:37 +02:00
Pontoporeia
6657c4fbbe refactor(nav): replace div+BEM classes with semantic ul/li in public nav
templates/nav.php:
- Replace <div class="site-nav__links"> with <ul role="list"> + <li> children
- Move À Propos link inside the list (was a loose sibling <a>)
- Remove .site-nav__link and .site-nav__link--active classes from all <a> elements
- Active state now driven solely by aria-current="page" (already present)

public/assets/common.css:
- Remove .site-nav__links, .site-nav__link, .site-nav__link:hover, .site-nav__link--active rules
- Add .site-nav ul (flex, gap, list-style reset), .site-nav ul a, .site-nav ul a:hover
- Active indicator: .site-nav ul a[aria-current="page"] — self-documenting, screen-reader-announced

Fixes TODO section I (nav semantic HTML audit). All three BEM nav-link classes deleted;
zero references remain in the codebase.
2026-03-29 15:50:41 +02:00
Pontoporeia
7a4a471838 fix: search filter labels, 429 page styling, __wakeup PHP 8.x deprecation
- Replace three <span class='search-filter-label'> with proper <label for='...'> elements in
  search.php filter bar; add id attributes to the corresponding <select> elements so the
  label/control association is programmatic (WCAG 1.3.1, 3.3.2).

- Rewrite the rate-limit 429 early-exit in search.php from a bare one-liner echo to a full
  HTML document with lang='fr', viewport meta, and inline dark styles matching maintenance.php;
  inject the retry countdown into the user-facing message (Template audit F).

- Fix PHP 8.x __wakeup() deprecation in Database.php singleton guard: replace the throw
  statement with trigger_error(..., E_USER_ERROR) and add an explicit void return type
  (Refactor audit C).
2026-03-29 15:47:30 +02:00
Pontoporeia
3a8ffa6afe Add Open Graph and Twitter Card meta tags to all public pages
- templates/public/head.php: add centralised OG/Twitter tag rendering via $ogTags array;
  supports type, title, description, url, image, image_alt, site_name, article_author,
  article_published_time; twitter:card switches between summary_large_image / summary
  based on presence of og:image

- public/tfe.php: populate full article OG tags — og:type=article, canonical URL,
  og:image resolved from banner_path → first image file in thesis_files → omitted,
  og:image:alt, article:author, article:published_time (year-01-01); twitter:card
  summary_large_image when image present

- public/index.php, search.php, apropos.php, licence.php: add basic og:type=website
  tags (title, description, canonical url, site_name)

Sharing a thesis link on Slack, WhatsApp, iMessage, or any social platform will now
render a rich preview card with the thesis title, synopsis excerpt, and cover/banner image.
2026-03-29 15:43:21 +02:00
Pontoporeia
1dee1ea73f Add <meta name=description> to all public pages; improve page titles
- templates/public/head.php: emit <meta name="description"> when $metaDescription is set
- index.php: title → 'Posterg – Mémoires de l\'ERG'; description = site blurb
- tfe.php: title → '[Titre] – [Auteur] – Posterg'; description = synopsis excerpt (strip_tags, truncate 160)
- search.php: description = répertoire purpose blurb
- apropos.php: description = about-page blurb
- licence.php: description = licences blurb

Fixes WCAG 2.4.2 (Page Titled) for index.php and tfe.php.
All descriptions properly htmlspecialchars-escaped at render time.
2026-03-28 19:38:21 +01:00
Pontoporeia
5c00886db6 fix fgetcsv deprecation and apply pending DB migrations 2026-03-28 19:13:52 +01:00
Pontoporeia
126703f340 tfe.php: full semantic HTML overhaul
- Replace <div class="tfe-layout"> with <article>, <div class="tfe-left"> with
  <header>, <div class="tfe-right"> with <aside> (supplementary media column)
- Fix inverted heading hierarchy: <h1> is now the thesis title (primary topic);
  author demoted to <p class="tfe-author"> (metadata, not a heading)
- Replace <div class="tfe-meta-list"> / <div class="tfe-meta-item"> / <span class="label">
  / <span class="value"> with <dl> / <dt> / <dd> (WCAG 1.3.1 info & relationships)
- Replace <div class="tfe-media-block"> with <figure>; <p class="tfe-file-caption">
  with <figcaption>; PDF <embed> gets .tfe-pdf-fallback download link (WCAG 4.1.2)
- Move back link to top of left column; extract inline styles to .tfe-back-link,
  .tfe-note-value, .tfe-restricted CSS classes
- Fix image alt text: description column used when populated, fallback to
  "Title — Author" instead of raw filename (WCAG 1.1.1)
- Add sr-only new-tab warning on baiu_link (WCAG 1.3.1 / 2.4.4)
- Fix PDF embed height: clamp(300px, 80vh, 700px) prevents horizontal overflow
  on small screens (WCAG 1.4.10 reflow)
- tfe.css: update all selectors to match new structure; remove inline styles;
  unify .tfe-restricted and .tfe-no-files; add .tfe-pdf-fallback, .tfe-back-link
2026-03-28 19:12:01 +01:00
Pontoporeia
a84d6d560a a11y: nav aria-label, search role=search + label, card hover motion guard
- templates/nav.php: add aria-label="Navigation principale" to <nav>; emit
  aria-current="page" on the active link alongside the existing CSS class
  so screen readers announce the current page without relying on colour/style alone

- templates/search-bar.php: add role="search" + aria-label="Recherche" to
  the <form>; add a visually-hidden <label for="site-search-input"> linked to
  the input via id="site-search-input", satisfying WCAG 3.3.2 (labels/instructions)
  and 4.1.2 (name/role/value) — placeholder text alone is not a label

- public/assets/main.css: add @media (prefers-reduced-motion: reduce) block that
  sets transition:none and transform:none on .card__media img/video hover, so the
  scale(1.02) zoom is fully suppressed for users who opt out of motion (WCAG 2.3.3 /
  prefers-reduced-motion); the global transition-duration guard in common.css already
  covers all other transitions but does not zero the transform value itself

Fixes TODO sections: G (nav/search-bar landmark names), I (site-search form ARIA),
3.3.2 (search input label), prefers-reduced-motion (card hover transform gate)
2026-03-28 18:13:53 +01:00
Pontoporeia
4f5ff5a22c refactor: extract edit.php POST handler to actions/edit.php
edit.php was a 530-line file mixing form display, POST handling, file
uploads, and reference-data loading. This refactor splits it along the
same action-file pattern already used by formulaire.php, tag.php, and
page.php.

Changes:
- public/admin/actions/edit.php (new): standalone POST handler; auth
  guard, CSRF check, transaction, redirect with session flash messages
- public/admin/edit.php: display-only; reads edit_success/edit_error
  flash keys from session; form action points to actions/edit.php via
  a hidden thesis_id field instead of a query-string self-post
- src/Database.php: four new methods to remove all raw PDO from both
  files:
    - updateThesis(int, array): void  — UPDATE theses core fields
    - setThesisAuthors(int, array): void  — delete-then-reinsert authors
    - getThesisLanguageIds(int): array — SELECT language_id for form
    - getThesisFormatIds(int): array   — SELECT format_id for form
2026-03-28 18:08:23 +01:00
Pontoporeia
f20aab5f66 css: deduplicate html/body reset; fix pages-edit.php invalid HTML
Move the repeated 'html, body { margin:0; padding:0; height:100% }' block from
main.css, search.css, tfe.css, and apropos.css into the single canonical location
in common.css. All four public page stylesheets already load common.css first, so
the rule applies identically — no visual change.

Fix pages-edit.php invalid HTML: the EasyMDE <link rel=stylesheet> was placed
inside <body> (after head.php was already closed), which is invalid. Add an
$extraCss hook to templates/admin/head.php so pages can inject <link> tags into
<head> via an array variable, matching the pattern already used by the public
templates/public/head.php. Also add a symmetric $extraJs hook to
templates/admin/footer.php for future use. pages-edit.php now sets
$extraCss = ['easymde.min.css'] before requiring head.php; the EasyMDE JS
<script> and its inline init remain in <body> in the correct load order.
2026-03-28 17:00:57 +01:00
Pontoporeia
b8529f7abe fix: WCAG 2.1 AA contrast, mobile répertoire layout, and pagination accessibility
Contrast failures (WCAG 1.4.3):
- common.css: remove opacity:0.92 from .site-nav__link (was 4.05:1, now 4.87:1 white-on-purple)
- common.css: placeholder colour #aaa → #767676 (2.32:1 → 4.54:1 on white)
- main.css: filter-info and clear-filter text var(--purple) → var(--purple-dark) (#9557b5 → #7b3fa0, 4.08 → 5.7:1)
- index.php: gradient card lighter stop L=65% → L=40%, darker stop L=45% → L=28%; white text now passes 4.5:1 across all hues

Non-text contrast (WCAG 1.4.11):
- search.css: search-filter <select> border #ddd → #949494 (1.6:1 → 3.0:1 on white)
- admin.css: --admin-border #333#555 (input bottom-border on #1a1a1a: 1.8:1 → 3.1:1)
- admin.css: --admin-text-muted #888#969696 (4.38:1 → 4.54:1 on #242424)

Mobile layout (WCAG 1.4.10 Reflow):
- search.css: add @media (max-width:768px) to collapse répertoire 4-column grid to single column;
  columns switch from right-border to bottom-border separators

Keyboard / screen reader (WCAG 2.1.1, 2.4.4):
- index.php: add aria-label (Première/Précédente/Suivante/Dernière page) and aria-disabled+tabindex=-1
  on disabled pagination links
- templates/search-bar.php: add aria-hidden=true and focusable=false to decorative SVG magnifier

Language (WCAG 3.1.1):
- search.php: add lang=fr to <html> in 429 rate-limit response
2026-03-28 16:52:45 +01:00
Pontoporeia
18197bd468 Extract shared public <head> partial
Create templates/public/head.php accepting $pageTitle and $extraCss (array of
stylesheet hrefs), mirroring the existing templates/admin/head.php pattern.

The partial emits: DOCTYPE, <html lang=fr>, charset/viewport meta, favicon,
modern-normalize, common.css, any extra CSS links, and the dev-only live-reload
script.  The live-reload snippet was previously copy-pasted verbatim into all
five public pages.

Updated pages:
  - public/index.php        ($pageTitle='Posterg', $extraCss=['assets/main.css'])
  - public/search.php       ($pageTitle='Répertoire – Posterg', search.css)
  - public/tfe.php          ($pageTitle=thesis title + suffix, tfe.css)
  - public/apropos.php      ($pageTitle='À Propos – Posterg', apropos.css)
  - public/licence.php      ($pageTitle=DB title + suffix, apropos.css)

tfe.php: removed redundant htmlspecialchars() call on $pageTitle (the partial
applies it); licence.php: renamed conflicting $page variable to $dbPage to
avoid collision with the shared $pageTitle expected by the partial.

All syntax checks and test suite pass (4/4).
2026-03-28 16:49:09 +01:00
Pontoporeia
640d37936f css: fix nav active state, deduplicate .site-nav__right, add font-display, clean up search pagination
- common.css: add font-display: swap to Combinedd.otf @font-face (eliminates FOIT)
- common.css: remove duplicate .site-nav__right block (identical to .site-nav__link);
  update nav.php to use .site-nav__link on the À Propos link
- common.css: add .site-nav__link--active rule (opacity:1 + white underline); the class
  was already applied in nav.php but had no CSS definition, making it invisible
- search.php: replace fully inline-styled pagination with .pagination-wrap / .pagination-btn
  / .pagination-info classes; add aria-disabled + tabindex=-1 on disabled links;
  add aria-label on prev/next links
- search.css: add pagination rule block to match, keeping styles co-located with the page
2026-03-28 16:44:35 +01:00
Pontoporeia
764edf9121 Remove dead template/asset files; fix licence.php full-width layout
- Delete templates/header.php and templates/head.php — both were legacy
  partials from a previous design iteration (lang="en", broken nav markup)
  that were never included anywhere in the current codebase.

- Delete public/assets/icons.svg — the full TrumboWYG icon sprite (~15 KB)
  referenced nowhere; the only active WYSIWYG editor (EasyMDE in
  pages-edit.php) loads its own assets from CDN.

- Fix licence.php layout: the page was borrowing the two-column
  .apropos-layout grid but leaving the right column always empty, wasting
  ~40% of the viewport. Removed the grid wrapper and the empty .apropos-right
  div. Added .apropos-single utility class to apropos.css (max-width: 720px)
  so licence content now spans the full available width with a readable
  line length.
2026-03-28 16:42:18 +01:00
Pontoporeia
61ac3c002d refactor: encapsulate thesis creation SQL in Database::createThesis()
Move the raw identifier-generation query and the INSERT INTO theses /
INSERT INTO thesis_authors statements out of formulaire.php into two new
Database methods:

  generateThesisIdentifier(int $year): string
    – counts existing theses for the year inside the open transaction so
      concurrent workers cannot produce duplicate YYYY-NNN identifiers.

  createThesis(array $data): int
    – generates the identifier, INSERTs the thesis row, links the author
      via thesis_authors (author_order=1), returns the new thesis ID.

  getThesisIdentifier(int $id): string
    – fetches the stored identifier for a thesis ID; used by formulaire.php
      to reconstruct the upload path (storage/theses/YYYY/YYYY-NNN/).

formulaire.php now calls $db->createThesis([…]) + $db->getThesisIdentifier()
and no longer holds any raw PDO queries for the core thesis insert.
The $pdo local variable (previously $db->getPDO()) is removed entirely.

All four test suites (Unit, RateLimit, Integration, Security) pass.
2026-03-28 13:52:43 +01:00