Commit Graph

185 Commits

Author SHA1 Message Date
Pontoporeia
41629398d3 Extract ThesisEditController from admin/edit.php and actions/edit.php
src/ThesisEditController.php (285 lines) centralises all data-fetching and
mutation logic for the thesis-edit workflow:

  load(int $thesisId): array
    Fetches the thesis row, current language/format/jury selections, and all
    lookup tables (orientations, AP programmes, finality types, languages,
    formats, licences, access types) in one call.  Returns a flat view-variable
    array that the dispatcher extracts directly.

  save(int $thesisId, array $post, array $files): void
    Runs the full edit inside a transaction: thesis metadata, authors, jury,
    languages, formats, tags.  Banner upload/removal is handled outside the
    transaction (filesystem op).  Rolls back and re-throws on any failure.

  static autofocusFieldForError(string $msg): ?string
    Centralises the WCAG 3.3.1 exception-message → field-name mapping that
    was previously duplicated inline in actions/edit.php.

Dispatcher changes:
  admin/edit.php      191 → 162 lines  (pure view + ThesisEditController::create() + load())
  actions/edit.php    153 →  53 lines  (CSRF guard + ThesisEditController::save() call)

Follows the same pattern as SearchController and SystemController.
2026-04-06 15:33:08 +02:00
Pontoporeia
40cb119448 Extract SystemController: centralise system page data logic, eliminate frag_ helper duplication
- Add src/SystemController.php (452 lines) encapsulating:
  - runStatusChecks(): nginx, php-fpm, HTTP ping, SQLite DB, storage, maintenance flag
  - getStatusData() / getPhpInfo() / getDiskInfo() with SystemCache TTL delegation
  - getLogData(tab, n): log file tail reading + file metadata
  - getNginxConfigData(): live-then-local nginx config reading
  - Static helpers: logLineClass(), nginxLineClass(), statusLabel(), statusClass(),
    humanBytes(), diskColor() — shared by both entry points
  - invalidateAll() for ?refresh=1 cache busting

- Rewrite admin/system.php: 582 → 282 lines
  - All free functions (safeExec, systemdStatus, localHttpCheck, humanBytes,
    statusLabel, statusClass, logLineClass, nginxLineClass, readLogTail) removed
  - Data sections replaced by controller method calls
  - View template unchanged; now calls SystemController::statusClass() etc. directly

- Rewrite admin/system-fragment.php: 213 → 137 lines
  - All duplicated frag_readLogTail(), frag_logLineClass(), frag_nginxLineClass()
    helpers removed
  - Now instantiates SystemController and delegates getLogData()/getNginxConfigData()
  - Identical rendering logic preserved; constant references updated to
    SystemController::LOG_FILES and SystemController::ALLOWED_LINES

No behaviour change; no CSS/JS changes.
2026-04-06 15:33:08 +02:00
Pontoporeia
9a58b97cb8 Extract SearchController from public/search.php
Move all data-fetching and request logic out of the 285-line search page
into src/SearchController.php:

- SearchController::create() — static factory; builds RateLimit + Database
  dependencies, sends HTTP 429 (and exits) if rate limit is exceeded,
  runs probabilistic cleanup, returns ready instance
- SearchController::handle() — sanitises GET params (query/year/orientation/
  ap_program/keyword), runs all DB queries (searchTheses, countSearchResults,
  getAvailableYears, getAllOrientations, getAllAPPrograms, getUsedTags,
  getPublishedAuthors), builds alphabetical author→id map, assembles
  OG/meta tags, returns a flat array of view variables
- Rate-limit 429 HTML response moved into private sendRateLimitResponse()

public/search.php is now a 6-line dispatcher:
  require SearchController; extract(SearchController::create()->handle());
followed by the unchanged view template (162 lines total, was 285).

The view template is byte-for-byte equivalent: same HTML, same variable
names, same pagination partial include.
2026-04-06 15:33:08 +02:00
Pontoporeia
c3a02e0aaa system.php: extract inline JS and style= attrs into separate assets
Move the ~130-line $extraJsInline heredoc from admin/system.php into a
static file public/assets/js/system.js, loaded via $extraJs so the
template footer emits a normal <script src=…>.

Replace 4 inline style= attributes with named CSS modifier classes in
system.css:
  - style="margin:0;border:none;padding:0" on .srv-section-title
    → .srv-section-title--compact
  - style="margin-bottom:.75rem" on sub-heading <h3>
    → .srv-section-title--sub
  - style="margin-bottom:0" on .php-grid
    → .php-grid--flush
  - style="font-size:.84rem;color:var(--text-secondary)" on <label>
    → .log-toolbar label rule in system.css

The one remaining inline style (--disk-pct / --disk-color CSS custom
properties on .disk-bar) is intentionally kept: it carries PHP runtime
values that cannot be expressed in a static stylesheet.
2026-04-06 15:33:08 +02:00
Pontoporeia
9637114f6b Clean up flash key legacy code and extract import.php inline styles
App::consumeFlash() had 18-line legacy fallback chains reading from seven old
session keys (error, admin_error, edit_error, form_error, success,
admin_success, edit_success) that were written by no code in the codebase.
All action handlers have used App::flash() -> _flash_error / _flash_success
since the App class was introduced. Removed the dead fallbacks; consumeFlash()
is now 4 lines.

admin/import.php was the last admin template with inline style= attributes.
Extracted four of them to named CSS classes in admin.css:
- admin-error-list   — error <ul> spacing (was style="margin:.5rem 0 0;padding-left:1.2rem")
- admin-file-hint    — <small> display + margin (was style="margin-top:.5rem")
- admin-import-results        — results panel margin (was style="margin-top:2rem")
- admin-import-results__title — <h2> typography (was multi-property inline style)

Closes the 'unify flash message keys' item in todo/02-php-components.md and
the import.php inline style item in todo/01-css-semantic-refactor.md.
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
4c3f71b6e4 Extract apropos contacts/credits to config/apropos.php
Names, roles, emails, and credits on the À propos page were hardcoded
directly in apropos.php HTML. To update a contact meant editing a
template file — risky for non-developers and easy to introduce a typo
or broken mailto link.

Changes:
- config/apropos.php: new config array with erg_url, contacts[] (name,
  role, email per person) and credits[] (label/value pairs); follows
  the same pattern as config/admin_credentials.php
- public/apropos.php: loads config via require; aside section now loops
  over $apropos['contacts'] and $apropos['credits'] with htmlspecialchars
  throughout; hardcoded HTML strings removed entirely

Also audited todo/02-php-components.md and marked 8 stale items as done:
all 5 form field partials were already implemented and in use, the
flash-message consolidation was already handled by App::consumeFlash(),
and the RateLimit cache dir was already at storage/cache/rate_limit
(excluded from deploy rsync).
2026-04-06 15:33:08 +02:00
Pontoporeia
94e9060dc7 WCAG 4.1.2: add WebVTT caption support for <video> elements on tfe.php
Problem: <video> elements on tfe.php had no <track kind="captions"> element,
violating WCAG 4.1.2 (name, role, value) for video content.

Changes:
- public/tfe.php: collect all text/vtt files from the thesis file list before
  rendering; skip standalone rendering of .vtt entries; for each MP4 emit a
  <track kind="captions" srclang="fr" label="Sous-titres" default> pointing
  to the N-th VTT file (N-th video paired with N-th caption in document order)
- public/media.php: add text/vtt to allowed MIME list; normalise finfo
  text/plain -> text/vtt for .vtt files; add vtt branch to cache/header
  block (Content-Type: text/vtt; charset=utf-8, 1-day cache)
- public/admin/actions/formulaire.php: allow .vtt uploads (text/vtt MIME,
  vtt extension); normalise text/plain finfo result; set file_type='caption'
  for VTT files so they are distinguishable from other thesis files
- public/admin/add.php: extend files field accept attr to include .vtt;
  update hint text to document the VTT sidecar convention

VTT files uploaded under theses/ inherit the same access_type visibility
gate in media.php as all other thesis content (403 for access_type_id=3).
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
769d56fabc wcag: fix 2.4.4 duplicate link text on home page cards
Add <span class="sr-only">, YEAR</span> to each thesis card <p> in
public/index.php. Screen readers now read "Author – Title, 2024" instead
of bare "Author – Title", so two theses sharing the same title produce
distinct accessible names (WCAG 2.4.4 Link Purpose — In Context).

Also audit and close WCAG 2.4.3: the tfe.php back link (<a class="tfe-back-link">
← Retour</a>) is already the first child of <header class="tfe-left">
in DOM order, preceding <h1 class="tfe-title">. No code change needed;
TODO item marked done.
2026-04-06 15:33:08 +02:00
Pontoporeia
fe1f8629ea rename admin-submit-wrap → admin-form-footer across all templates and CSS
- Updated 6 admin templates: add.php, edit.php, login.php, account.php,
  import.php, pages-edit.php — replaced <div class="admin-submit-wrap">
  with <div class="admin-form-footer">
- Updated 8 CSS selectors in admin.css:
  - .admin-form-footer { margin-top/padding-top } (was .admin-submit-wrap)
  - .admin-form > div:not(.admin-form-footer) grid exclusion guard (×3)
  - .admin-login-box .admin-form > div:not(.admin-form-footer) overrides (×2)
  - .admin-login-box .admin-form-footer compact spacing override
- No visual change; purely a semantic rename to a descriptive class name
- Also marked status-badge.php partial and WCAG 1.3.1 badge tasks as
  already-done in todo/02-php-components.md and todo/04-accessibility.md
  (partial + CSS were fully implemented but todo had not been updated)
2026-04-06 15:33:08 +02:00
Pontoporeia
62eee63f80 fix(admin): add aria-current nav indicator + fix undefined --admin-purple variable
WCAG 1.4.1 — Active nav link had no non-colour indicator in the admin panel.
Public nav already had border-bottom via common.css; admin nav had nothing.

admin.css:
- Add `[aria-current="page"]` rule on admin nav links:
    border-bottom: 2px solid currentColor; padding-bottom: 1px
  This gives a visible underline as a non-colour signal for the active page.

- Fix `--admin-purple` undefined CSS variable in pagination button hover.
  The variable was referenced but never defined in variables.css (which was
  refactored to use --accent-primary / --accent-secondary). Replaced both
  border-color and color usages with var(--accent-primary) (#9557b5 — same
  value), restoring the intended purple hover tint on pagination buttons.

todo/01-css-semantic-refactor.md:
- Audited ~15 pending CSS/HTML tasks; all were already implemented.
  Marked as done: .admin-main, .admin-page-title, .admin-form-row,
  .admin-label, .admin-input/select/textarea, .admin-table, .admin-fieldset,
  tfe.css class replacements, search.css h2 selector, admin-alert replacement,
  login.php/edit.php inline style removal, form partial hints (<small>).

todo/04-accessibility.md:
- Marked WCAG 1.4.1 admin nav and --admin-purple audit items as completed.
2026-04-06 15:33:08 +02:00
Pontoporeia
234d7bae40 admin/index.php: add server-side pagination (25/page)
- Add Database::getThesesListCount(array $filters) — runs the same WHERE
  clauses as getThesesList() but with COUNT(DISTINCT t.id); used to compute
  total pages without loading all rows.
- Extend Database::getThesesList() with $limit/$offset parameters; when
  $limit > 0 appends LIMIT/OFFSET and re-binds positional params individually
  to avoid the PDO mixed-style restriction.
- Fix getThesesList() SELECT: add LEFT JOIN access_types + at.name as
  access_type — the column was referenced in the template but never fetched.
- Wire admin/index.php: read ?page=, compute $totalPages/$offset, pass
  $perPage=25 + $offset to getThesesList(); include pagination.php partial
  below the table with filter-preserving $baseParams.
- Add result-count line (<p class="admin-list-meta">) showing "X–Y sur Z TFE"
  when multiple pages exist.
- Add .admin-body .pagination-wrap / .pagination-btn / .pagination-info styles
  to admin.css (scoped to .admin-body to avoid colliding with public pages).
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
fde05da493 common.css: taller header, larger nav text (header height -10%) 2026-04-06 15:32:59 +02:00
Pontoporeia
b2ec15372c common.css: taller header, larger nav text
- header nav padding 1rem → 1.6rem (top/bottom)
- logo link font-size 0.95rem → 1.1rem
- nav link font-size 0.85rem → 0.95rem
2026-04-06 15:32:59 +02:00
Pontoporeia
da6d06f65a common.css: header text-shadow — purple glow, 0 offset, 12px blur, 65% opacity 2026-04-06 15:32:41 +02:00
Pontoporeia
c68e355de7 common.css: add subtle text-shadow to header nav text
0 offset, 8px blur, rgba(0,0,0,0.35) — no directional shadow, just a
soft glow that improves legibility of white text against the lighter
portions of the gradient header.
2026-04-06 15:32:41 +02:00
Pontoporeia
d85fb22cfc admin: center main content with margin-inline: auto 2026-04-06 15:32:41 +02:00
Pontoporeia
f18e3381ea admin.css: rewrite from scratch using only variables.css tokens
The file had accumulated severe corruption in its lower half (garbled
selector text, variable names spliced into property values, orphaned
declarations, broken nesting) alongside hardcoded hex colours throughout.

Rewrote the entire file cleanly:
- Every colour is now a var() referencing a token defined in variables.css:
  --accent-primary/secondary/foreground, --accent-blue/green/yellow/red,
  --bg-secondary/tertiary, --border-primary, --text-primary/secondary/tertiary,
  --error, --warning, --success, --accent-muted.
- Zero raw hex values remain in admin.css.
- Removed the corrupted/dead CSS from the bottom half and reconstructed
  all selectors from what the templates actually use (audited via grep).
- Fixed structural issues: broken border shorthand, nested rules that
  were not valid CSS, orphaned declaration blocks.
- New/restored rules: .admin-maintenance-bar (was corrupted),
  .status-access variants (was corrupted), .admin-section-title--danger,
  .admin-danger-zone, .admin-account-status (all reconstructed cleanly).
- .admin-btn--warning and .admin-btn--danger now use var(--accent-yellow)
  and var(--accent-red) instead of hardcoded dark hex values.
- .admin-btn-remove hover now uses var(--error) instead of #e55.
- .admin-btn-unpublish now uses var(--bg-secondary)/var(--text-tertiary)
  instead of hardcoded grey hex values.
- select option background colours removed (browser chrome, not styleable
  cross-platform).

Templates: replace 4 inline var(--admin-text-muted) with var(--text-secondary)
in index.php, thanks.php, import.php.
2026-04-06 15:32:41 +02:00
Pontoporeia
871e919efa system.css: use only variables.css tokens, remove undefined custom properties
Replace the two undefined variables that had crept in:
- var(--admin-border) → #555  (in .log-output border)
- var(--admin-text-muted) → #969696  (inline style on log toolbar label,
  in both system.php and system-fragment.php)

Revert the incorrect intermediate attempt that mapped dark-UI hex values
to light-theme tokens (--bg-primary: #fff, --border-primary: #ddd, etc.)
and also revert the .admin-body override block that was added to
variables.css — variables.css is shared and must not have per-component
overrides.

All remaining var() calls in system.css now reference tokens that exist
in variables.css:
  --accent-primary, --accent-green, --error, --warning, --success,
  --text-tertiary
The dark surface colours (#1a1a1a, #242424, #0d0d0d, #555, #969696, etc.)
stay as literal hex values, consistent with how admin.css handles them.
2026-04-06 15:32:41 +02:00
Pontoporeia
b981223ff4 admin/system: fetch()-based tab switching, no full-page reload
Add system-fragment.php — a thin authenticated endpoint that returns only
the tab-panel HTML (toolbar + meta + log/nginx-config output) for a given
?tab=&n= combination. No page shell, no status section, no DB queries.

system.php changes:
- Tab <a> elements gain data-tab= attributes used by JS to identify the
  target without parsing hrefs.
- Tab panel content wrapped in <div id=sys-tab-panel data-tab= data-n=>
  which JS uses as both the swap target and its own state store.
- JS rewritten: tab clicks and lines-select changes call loadPanel()
  which fetch()es system-fragment.php, swaps innerHTML, updates active
  tab ARIA attributes, and pushes state via history.pushState.
- Browser back/forward handled via popstate listener.
- bindPanelControls() re-wires the lines-select and copy-to-clipboard
  button after every innerHTML swap (event delegation not feasible here
  because log-output is replaced wholesale).
- fetch() failure falls back to window.location.href (full page load).
- Tabs without JS still work: <a href> links go to system.php?tab=…
  as before.

system-fragment.php:
- Requires AdminAuth::isAuthenticated(); returns 403 on failure.
- Validates tab and n params against the same whitelist as system.php.
- All helper functions namespaced with frag_ prefix to avoid redeclaration
  if PHP ever includes both files in the same process.
- Renders identical HTML to the corresponding section in system.php.

system.css:
- #sys-tab-panel gets min-height:8rem and position:relative to prevent
  layout jump during fetch.
- .sys-panel-loading: opacity 0.4 + pointer-events:none + subtle
  diagonal-stripe ::after overlay with shimmer animation.
2026-04-06 15:32:41 +02:00
Pontoporeia
c86781b9be admin/system: move status panel above tabs, add collapse toggle
Status (services, PHP env, disk) is now always visible above the log/config
tab bar rather than being one of the tab targets:

- Status section rendered unconditionally above <nav class="sys-tabs">.
- Services grid, PHP info grid and disk bar grouped inside a collapsible
  <section> with a header row containing the cache-freshness badge and a
  toggle button (▲ Réduire / ▼ Développer).
- Collapse state persisted in localStorage so the preference survives
  page reloads (e.g. when switching log tabs).
- Tab bar now only contains the three log tabs + nginx config; the 'Statut'
  tab is removed. Legacy ?tab=status URLs fall through to nginx_access.
- PHP/disk sub-sections laid out in a 2-col grid inside the status panel;
  responsive single-col below 700px.
- system.css: new .sys-status-section / .sys-status-header /
  .sys-status-toggle / .sys-status-meta rules added.
- aria-current="page" added to active tab links.
- todo/03-system-cache.md: all items marked done; notes added explaining
  why log caching was deliberately omitted.
2026-04-06 15:32:41 +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
ba36725111 Split TODO.md into todo/ folder by topic (completed tasks removed) 2026-04-06 15:32:41 +02:00
Pontoporeia
d78befe622 restore TODO.md: recover full historical TODO from 9108c4069d, append CSS color variables section (2026-04-02) 2026-04-06 15:32:41 +02:00
Pontoporeia
7e0ac45a65 Changed colors to the shared colors 2026-04-06 15:32:34 +02:00
Pontoporeia
758bdce669 refactor: unify CSS color variables across public and admin
- Add new standardized color variables in variables.css:
  - Public/light theme: --bg-primary, --bg-secondary, etc.
  - Admin/dark theme: --admin-bg, --admin-bg-alt, --admin-text, etc.
  - Gradient colors: --gradient-start (#3C856C), --gradient-2 (#60ECB4), --gradient-3 (#E390FF), --gradient-4 (#9557B5)
  - Shared: --success, --error, --warning, --accent-primary, --accent-secondary
- Update all CSS files to use new variables
- Keep admin-specific variables for dark theme sections
2026-04-02 17:28:32 +02:00
Pontoporeia
ae499e45b5 refactor: unify CSS color variables across public and admin
- Add new standardized color variables in variables.css:
  - Public/light theme: --bg-primary, --bg-secondary, etc.
  - Admin/dark theme: --admin-bg, --admin-bg-alt, --admin-text, etc.
  - Gradient colors: --gradient-start (#3C856C), --gradient-2 (#60ECB4), --gradient-3 (#E390FF), --gradient-4 (#9557B5)
  - Shared: --success, --error, --warning, --accent-primary, --accent-secondary
- Update all CSS files to use new variables
- Keep admin-specific variables for dark theme sections
2026-04-02 17:24:43 +02:00
Pontoporeia
f7babf9e96 refactor: unify CSS color variables across public and admin
- Add new standardized color variables in variables.css:
  - Public/light theme: --bg-primary, --bg-secondary, etc.
  - Admin/dark theme: --admin-bg, --admin-bg-alt, --admin-text, etc.
  - Gradient colors: --gradient-start (#3C856C), --gradient-2 (#60ECB4), --gradient-3 (#E390FF), --gradient-4 (#9557B5)
  - Shared: --success, --error, --warning, --accent-primary, --accent-secondary
- Update all CSS files to use new variables
- Keep admin-specific variables for dark theme sections
2026-04-02 17:23:58 +02:00
Pontoporeia
bf2594112b fix serve recipe: filter output to Development Server start + [200] requests only
refactor: unify CSS color variables across public and admin

- Replace old variable structure with new standardized naming:
  - Background: --bg-primary, --bg-secondary, --bg-tertiary, --bg-active
  - Text: --text-primary, --text-secondary, --text-tertiary
  - Border: --border-primary, --border-secondary
  - Status: --success, --error, --warning
  - Accent: --accent-primary, --accent-secondary, --accent-foreground, --accent-muted
- Remove admin-specific color variables (--admin-*)
- Update all CSS files to use shared variables:
  - variables.css, common.css, main.css, admin.css
  - tfe.css, search.css, apropos.css, system.css, colors.css
2026-04-02 17:22:51 +02:00
Pontoporeia
ba7814c6dc feat: system page caching via SystemCache + system_cache SQLite table
Add a TTL-based cache for the expensive checks on the admin system page,
eliminating repeated systemctl subprocess calls (~4×~100ms), curl self-pings
(~200-500ms), disk_*_space() and PHP ini reads on every page load.

Changes:
- storage/migrations/007_system_cache.sql: new migration creating the
  system_cache table (key TEXT PK, value TEXT, updated_at INTEGER)
- storage/schema.sql: system_cache table added before pages table
- Applied migration to live storage/posterg.db
- src/SystemCache.php: new class with get/set/isStale/ageSeconds/invalidate;
  uses SQLite INSERT … ON CONFLICT upsert; no external dependencies
- src/Database.php: added getDatabasePath(): string accessor
- public/admin/system.php:
  - Bootstrap SystemCache at request start using the existing DB PDO handle
  - system_status: cached with 2-min TTL (systemctl + curl checks)
  - php_info: cached with 1-hour TTL (PHP ini values are runtime-constant)
  - disk_info: cached with 5-min TTL (total/free/used/pct tuple)
  - Logs section: unchanged — always reads live log tail per active tab
  - ?refresh=1 GET param invalidates all three cache keys before rendering
  - Status panel heading shows cache badge: ' Cache — il y a Xs' (hit)
    or '⟳ Actualisé' (miss/fresh), styled via new .sys-cache-badge rules
- public/assets/css/system.css: .sys-cache-badge / --hit / --miss styles
2026-04-02 13:04:00 +02:00
Pontoporeia
592b1183db Unify flash messages: replace all legacy session key writes with App::flash()
All admin action files (account, tag, page, edit, visibility, maintenance,
publish, formulaire) now call App::flash('error'|'success', ...) instead of
writing to raw per-page session keys ($_SESSION['error'], 'admin_error',
'edit_error', 'admin_success', 'edit_success', 'form_error').

All admin display pages (add, edit, account, tags, pages, index) now include
templates/partials/flash-messages.php instead of manually reading and
unsetting the legacy session keys and inlining their own alert HTML.

App::consumeFlash() already drained all legacy key variants as a safety net,
so the partial works correctly whether called from pages that were already
migrated or any remaining stragglers. No behaviour change for end users.
2026-04-02 12:57:36 +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
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