Commit Graph

47 Commits

Author SHA1 Message Date
Pontoporeia
547d581e26 Removed footer navbar 2026-04-08 14:14:37 +02:00
Pontoporeia
572ef75a1e répertoire: rename search.php, 6-column layout, HTMX filter, faded entries disabled, URL-shareable 2026-04-08 14:14:37 +02:00
Pontoporeia
088324cb80 Match Accueil.png mockup: nav layout, full-width search, section label 2026-04-08 14:14:37 +02:00
Pontoporeia
d51cd62088 Extract last inline style from header.php into admin.css
The SVG icon in the admin nav's public-site link had two inline styles:
  style="vertical-align:middle;margin-right:0.4em"

Moved to a new CSS rule:
  .admin-body header nav > a svg { vertical-align: middle; margin-right: 0.4em; }

templates/header.php now contains zero style= attributes.
The only remaining inline styles project-wide are:
  - dynamic gradient (hsl computed from $item['id']) in public/index.php — legitimately dynamic
  - --disk-pct/--disk-color custom properties in system.php — carry PHP runtime values
2026-04-06 15:33:08 +02:00
Pontoporeia
c2eff75789 WCAG 3.3.1: autofocus first invalid field on add/edit form validation failure
Add App::flashAutofocus(fieldName) and consumeAutofocus() to the thin App
helper so action handlers can identify which field caused a validation error
and the form page can move browser focus directly to it on reload.

Changes:
- src/App.php — flashAutofocus() stores field name in _flash_autofocus
  session key; consumeAutofocus() drains it and returns the name (or null)
- actions/formulaire.php — catch block maps exception messages to field
  names (auteurice, titre, synopsis, année, orientation, ap, finality,
  languages, tag, lien) and calls App::flashAutofocus()
- actions/edit.php — catch block maps common edit errors to field names
  and calls App::flashAutofocus()
- add.php — consumes the hint via App::consumeAutofocus() into
  $autofocusField; withAutofocus() helper merges autofocus=>true into
  $attrs for every field include; synopsis textarea gets inline autofocus
- edit.php — same pattern with inline ternary merges and textarea autofocus
- templates/partials/form/text-field.php — $attrs loop now emits bare
  attribute names (no ="...") when value === true, supporting autofocus,
  disabled, readonly etc. without special-casing
- templates/partials/form/select-field.php — same boolean-attr support
  added; $attrs variable initialised to [] when caller omits it

Closes WCAG 3.3.1 autofocus item in todo/04-accessibility.md.
2026-04-06 15:33:08 +02:00
Pontoporeia
6e68edfbff Fix WCAG 4.1.2 truncated select text + split admin/public favicons
- admin/edit.php: remove mb_strimwidth(60) truncation from access_type
  <select> option labels; full 'name — description' text is now the
  accessible name so screen readers get unambiguous option text (WCAG 4.1.2)

- public/assets/favicon.svg: new public favicon — brand-purple (#9557b5)
  rounded square with white 'P' lettermark; distinct from admin_favicon.svg
  (archive-restore Lucide icon in #c104fc) which is admin-only

- templates/head.php: favicon <link> now conditionally serves favicon.svg
  (public pages) or admin_favicon.svg (admin pages) based on $isAdmin;
  closes the open favicon task in todo/01-css-semantic-refactor.md

- todo/04-accessibility.md: mark WCAG 3.1.1 lang audit and WCAG 4.1.2
  select truncation items as done
- todo/01-css-semantic-refactor.md: mark favicon task as done
2026-04-06 15:33:08 +02:00
Pontoporeia
d9f94eeb13 a11y(jury-fieldset): fix WCAG 3.3.2, 4.1.2, 2.1.1 + audit 1.4.4/1.4.12
3.3.2 Labels or instructions
- Replace bare <label>Lecteur·ices :</label> (no 'for', no associated control)
  with <fieldset class="admin-jury-lecteurs"><legend>Lecteur·ices</legend>
  giving AT a proper programmatic label for the entire lecteur group

4.1.2 Name, role, value — Externe checkboxes lacked group context
- Add aria-label="Promoteur·ice — externe" on the promoteur Externe checkbox
- Add aria-label="Lecteur·ice N — nom" on every lecteur name input
- Add aria-label="Lecteur·ice N — externe" on every lecteur Externe checkbox
- All three attributes added to both PHP-rendered rows and the addJuryRow() JS
  that builds new rows dynamically

2.1.1 Keyboard — remove buttons already had aria-label; verified and updated
  label text to "Supprimer le lecteur·ice N" (consistent with new numbering)

CSS (admin.css)
- Add .admin-body fieldset fieldset.admin-jury-lecteurs rule: removes
  border/padding/background from the nested fieldset so it reads as a
  sub-group inside the outer jury fieldset, not a double-bordered card

Audit (no code change)
- WCAG 1.4.4: all font-size values use rem — no px text sizing anywhere
- WCAG 1.4.12: only overflow:hidden on media containers and .sr-only utility;
  no essential text content is clipped by text-spacing overrides
- WCAG 4.1.2 bulk JS: result is a redirect to flash-messages.php which already
  emits role="alert"/role="status" — no additional JS announcement needed
2026-04-06 15:33:08 +02:00
Pontoporeia
ff8e33727d admin: semantic HTML pass — checkbox fieldset, landmarks, dl/dt, autocomplete, inline styles
checkbox-list.php partial:
- Replace outer <div>/<label> with <div>/<span class="admin-row-label"> + inner
  <fieldset class="admin-checkbox-group"><legend class="sr-only"> to satisfy
  WCAG 1.3.1 (group label for multi-checkbox rows without duplicating visible text)
- Replace <div class="admin-checkbox-list"> with <ul>; each checkbox wrapped in <li>

admin.css:
- Drop .admin-checkbox-list; add .admin-body fieldset.admin-checkbox-group rules
  (border/padding reset so it doesn't inherit jury-fieldset box styling)
- Extend form-row label rule to span.admin-row-label
- .admin-inline-form + .admin-inline-form { margin-top:.35rem } replaces inline style
- .admin-input--inline / .admin-select--inline get width:160px (was inline style)
- .admin-tags-count + table th sizing via :has() replaces th inline styles

login.php: wrap content in <main id="main-content"> (missing landmark)

account.php:
- <div class="admin-account-status"> → <dl>; __label <span> → <dt>
- <div class="admin-danger-zone__description"> → <p>

index.php: <div class="admin-maintenance-bar"> → <aside role="status" aria-label="Statut du site">

add.php / edit.php: autocomplete="name" on author field, autocomplete="email" on
contact field (WCAG 1.3.5 / input purpose)

tags.php: all inline style= attributes removed (width, text-align, margin-top,
display:inline); all moved to CSS classes
2026-04-06 15:33:08 +02:00
Pontoporeia
e1ce900113 a11y: WCAG 2.5.5 target sizes + 2.5.3 label-in-name fixes
Increase touch/click target sizes to meet WCAG 2.5.5 (minimum 44×44px
for navigation, 32px for admin UI controls):

- main.css / search.css: pagination buttons 2rem → min-height/min-width
  2.75rem (44px). Changed display to inline-flex for proper centering.
- admin.css: .admin-btn-sm gains min-height: 2rem (32px) and switches
  to inline-flex so the constraint is respected.
- admin.css: .admin-btn-remove (jury ✕ buttons) gains min-height: 2rem
  and inline-flex display + explicit cursor:pointer.

WCAG 2.5.3 label-in-name — jury remove buttons already had aria-label;
wrap the visible ✕ glyph in <span aria-hidden='true'> so screen readers
hear only the aria-label, not the symbol:

- templates/partials/form/jury-fieldset.php: all three ✕ occurrences
  (static PHP blocks + JS-generated innerHTML string) wrapped.

WCAG 4.1.2 / semantic HTML:
- admin/index.php: add role='toolbar' aria-label='Actions groupées' to
  the bulk-actions bar.
2026-04-06 15:32:41 +02:00
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
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
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
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
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
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
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
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
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
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
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
42af4644c5 perf+a11y: WAL mode for SQLite, skip links, :focus-visible, .sr-only
SQLite performance (Database::__construct):
- PRAGMA journal_mode = WAL: eliminates full-DB read locks on write, safe
  for concurrent PHP-FPM workers
- PRAGMA synchronous = NORMAL: durable on commit without full fsync per write
- PRAGMA cache_size = -8000: ~8 MB page cache per connection

Accessibility foundation (WCAG 2.1 AA):
- common.css: add .sr-only utility, .skip-link (hidden until focused),
  global :focus-visible (2px purple outline, 2px offset),
  prefers-reduced-motion guard; remove bare outline:none from
  .site-search__input
- admin.css: same :focus-visible, skip-link, and motion guard scoped to
  admin purple; remove outline:none from .admin-input/.admin-select/
  .admin-textarea and .admin-filters select (both had :focus border rules
  already, so focus is still visually communicated)
- search.css: remove outline:none from .search-filter-select (already has
  :focus border-color rule)
- All 5 public pages (index, search, tfe, apropos, licence): add
  <a href="#main-content" class="skip-link"> as first child of <body>;
  add id="main-content" to <main>
- templates/admin/head.php: same skip link; aria-label="Navigation admin"
  on <nav>; id="main-content" on all 10 admin <main> elements

All 4 test suites pass (unit, integration, security, rate-limit).
2026-03-27 13:45:01 +01:00
Pontoporeia
37f3a07c6e admin: merge status + logs into unified system.php with instant tabs
Replace the separate /admin/status.php and /admin/logs.php pages with a
single /admin/system.php page organised around a tab bar.

- system.php — top-level tab bar: 'Statut' + one tab per log file
  (nginx accès, nginx erreurs, PHP-FPM).  Switching tabs is a plain
  href (?tab=…) so no JS required for navigation; the lines-selector
  SELECT triggers a location change on 'change' for instant reload
  without a submit button.
- Status tab preserves all existing service cards, PHP runtime grid,
  and disk-usage bar from the old status.php.
- Log tabs preserve line-count selector, file metadata bar, and
  per-line colour coding from the old logs.php.
- New: copy-to-clipboard button on each log output block (Clipboard
  API with textarea execCommand fallback).
- status.php / logs.php replaced with 301 redirect stubs so existing
  bookmarks and links keep working.
- templates/admin/head.php: 'Statut' + 'Journaux' nav items replaced
  with a single 'Système' item; active state covers all three page
  names for redirect compatibility.
2026-03-24 15:55:48 +01:00
Pontoporeia
20a633c0e2 Add admin account page for PHP password management
Implements the admin user management UI as a self-contained PHP password
change/set flow — no SSH or sudo required.

- public/admin/account.php: shows auth status (PHP hash present, credentials
  file path), password change form (requires current password when one exists,
  min 12 chars, confirm field), and a danger-zone form to delete the
  credentials file entirely
- public/admin/actions/account.php: CSRF-guarded POST handler; verifies
  current password via AdminAuth::login() before accepting a new one;
  generates bcrypt (cost 12) hash; writes config/admin_credentials.php
  atomically via a temp file + rename; regenerates session on success;
  redirects to /admin/login.php when credentials are deleted
- templates/admin/head.php: 'Compte' nav link added (active on account.php)
- public/assets/admin.css: .admin-account-status, .admin-section-title,
  .admin-field-hint, .admin-danger-zone component styles added

Note: the nginx htpasswd flow (manage-admin-users.sh) requires root on the
server and is intentionally kept as a CLI-only operation.
2026-03-24 15:52:00 +01:00
Pontoporeia
020bfa5a33 admin: add server log viewer; fix curl_close() PHP 8.5 deprecation in status.php
- public/admin/logs.php: new page tailing nginx error/access + PHP-FPM logs.
  Selector for log file and line count (50/100/200/500, default 100).
  Lines reversed (newest first), colour-coded by severity, numbered gutter.
  Graceful degradation when exec() unavailable or file unreadable (dev msg).

- templates/admin/head.php: 'Journaux' nav link added after 'Statut'.

- public/admin/status.php: remove curl_close() call deprecated in PHP 8.5
  (no-op since PHP 8.0); replace with unset($ch) to silence the warning
  that was leaking raw text above the page output.
2026-03-24 15:47:38 +01:00
Pontoporeia
c678b75494 Add admin server status page
New page /admin/status.php gives a real-time health dashboard:

- Services panel: nginx (systemctl), php-fpm (auto-detects versioned unit names),
  site HTTP ping (curl HEAD with latency), SQLite DB (exists/writable/row count/size),
  storage directory (writable, banner/cover file counts), maintenance-mode flag.
- PHP runtime panel: version, SAPI, memory_limit, upload_max_filesize, post_max_size,
  max_execution_time.
- Disk usage bar for the partition containing APP_ROOT (colour-coded: green/amber/red).
- All shell calls go through safeExec() which suppresses stderr and checks exit code;
  systemctl/curl unavailability degrades gracefully to 'unknown' without fatal errors.
- 'Statut' nav link added to templates/admin/head.php (active state on status.php).
2026-03-24 15:41:30 +01:00
Pontoporeia
92e344b757 feat: admin tag management, maintenance mode, TFE visibility states
Tags admin:
- Database: getAllTagsWithCount(), renameTag(), mergeTag(), deleteTag()
- public/admin/tags.php: table with inline rename/merge/delete forms, CSRF-guarded
- public/admin/actions/tag.php: routes on action=rename|merge|delete
- templates/admin/head.php: 'Mots-clés' nav link
- admin.css: admin-inline-form, admin-btn--sm/warning/danger variants

Maintenance mode:
- config/bootstrap.php: gate on MAINTENANCE_FLAG file; admin/ and maintenance.php exempt
- public/maintenance.php: 503 dark minimal page
- public/admin/actions/maintenance.php: enable/disable toggle
- public/admin/index.php: status bar with toggle button
- admin.css: admin-maintenance-bar styles

TFE Visibility (Libre/Interne/Interdit via existing access_type_id):
- migration 002_add_visibility.sql: seeds access_types if missing
- Database: setVisibility(), bulkSetVisibility(), getAccessTypes()
- public/media.php: blocks thesis files for access_type_id=3
- public/tfe.php: shows access_type, context_note; hides file panel for Interdit
- public/admin/edit.php: access_type_id select + context_note textarea; saves both
- public/admin/index.php: three-state badge (Libre/Interne/Interdit) per row
- public/admin/actions/visibility.php: single + bulk visibility action handler
- admin.css: status-access badge variants
2026-03-24 15:35:52 +01:00
Pontoporeia
d87348c388 feat: licence page, admin pages editor, license types, gradient card placeholders, latest-year home view
- Feature 1: public /licence.php fetches 'licenses' page from DB, renders Markdown
- Feature 1: nav.php adds 'Licence' link with active state
- Feature 2: Database::getPage(), savePage(), getAllPages() methods
- Feature 2: bundled src/Parsedown.php (MIT, zero-dependency)
- Feature 2: apropos.php now renders 'about' page content from DB via Parsedown
- Feature 2: admin/pages.php (list) + admin/pages-edit.php (EasyMDE editor)
- Feature 2: admin/actions/page.php (auth+CSRF+validation+save)
- Feature 2: admin/head.php adds 'Pages statiques' nav link
- Feature 3: storage/schema.sql seeds 8 CC license types
- Feature 3: storage/migrations/003_seed_license_types.sql (applied to live DB)
- Feature 3: Database::getLicenseTypes() / getAllLicenseTypes()
- Feature 3: admin/add.php + formulaire.php: license_id field on add form
- Feature 3: admin/edit.php: license_id field on edit form with raw FK lookup
- Feature 3: tfe.php: shows 'Licence :' meta row when non-null
- Feature 6: main.css: .card__media--gradient styles
- Feature 6: index.php: deterministic HSL gradient placeholder cards
- Feature 6: Database::getLatestYearTheses() + getLatestPublishedYear()
- Feature 6: index.php default home = random latest-year theses with info label
2026-03-24 13:12:48 +01:00
Pontoporeia
1fb9644d5a fix favicon 404s: add <link rel=icon> to all pages, nginx 204 for /favicon.ico 2026-03-02 16:08:45 +01:00
Pontoporeia
2110d2b916 Redesign UI to match target design images
- Flat purple-gradient nav bar with POSTERG/RÉPERTOIRE/À PROPOS links
- Full-width search bar with icon, bottom-border only, below nav
- Home: white bg, media card grid (thumbnail + author/title label below)
- Répertoire: 4-column index (Années/Catégories/Étudiantes/Mots-clés)
- TFE: 2-column layout (large text left, media right)
- À Propos: 2-column, large monospace text, new apropos.php page
- Admin: dark theme (#1a1a1a), purple gradient nav, bottom-border inputs
- New shared partials: templates/nav.php, templates/search-bar.php
- Rewrote all CSS: common, main, search, tfe, apropos, admin
2026-02-24 23:34:17 +01:00
Pontoporeia
d30153871f fix: resolve broken lib/ require paths in admin and normalise modern-normalize to .min.css 2026-02-24 23:19:18 +01:00
Théophile Gervreau-Mercier
73b0093b26 feat: rename memoire to tfe and improve styling
- Rename memoire.php to tfe.php throughout codebase
- Create dedicated tfe.css with rounded header/main/footer layout
- Move metadata (orientation, AP program, finality, keywords) to header
- Move back button from header to footer
- Create shared templates/head.php for common HTML head section
- Maintain rounded borders (40px) matching main site design
- Keep purple header (#9557b5), green main (#3c856b), dark footer (#222)
- Improve content readability with centered max-width layout
- Add responsive design for mobile devices
2026-02-12 12:46:51 +01:00
Théophile Gervreau-Mercier
9511bb93b5 feat: add year filter to main index
- Footer now displays all available years horizontally with scroll
- Click on year filters thesis list to that year
- Active year highlighted in footer
- 'Tous' link to reset filter
- Filter info banner shows when year selected with reset button
- Pagination preserves year filter
- Styled with horizontal scroll, smooth scrollbar
- Tests passing 
2026-02-12 12:26:32 +01:00
Théophile Gervreau-Mercier
87971f9c23 refactor: extract templates from public/
- Created /templates for main site (header.php, footer.php)
- Created /templates/admin for admin section (head.php, footer.php)
- Removed /public/includes and /public/admin/inc
- Updated all references in code and docs
- Tests passing 

Cleaner separation: /public only contains web-accessible files (PHP entry points + assets)
2026-02-12 12:15:41 +01:00