Commit Graph
100 Commits
Author SHA1 Message Date
Pontoporeia 4986fa74f4 add structured logging for admin/partage form submissions + migration system
- AppLogger: JSON-line logger in storage/logs/form-submissions.log
- Logs submissions (admin + partage) with IP, UA, thesis ID, author
- Logs errors with context (post keys, share slug)
- Migration runner (app/migrations/run.php) handles schema drift
- 001_add_objet_column.sql fixes production DB missing 'objet' column
- ThesisCreateController::getIdentifier() helper for logging
2026-04-24 23:03:49 +02:00
Pontoporeia decb9e2907 fix: replace mb_strlen/mb_substr in student-preview.php (mbstring unavailable) 2026-04-24 23:03:49 +02:00
Pontoporeia 9b4cb52617 fix: replace mb_strtolower with strtolower in admin import (mbstring unavailable in php8.4-fpm) 2026-04-24 23:03:49 +02:00
Pontoporeia 769beae4ee fix: drop hx-trigger once — rely on browser cache for dedup 2026-04-24 23:03:49 +02:00
Pontoporeia 743268cf1b fix: drop hx-trigger once — rely on browser cache for dedup 2026-04-24 23:03:49 +02:00
Pontoporeia 6eb111a6ab perf: htmx lazy popover with Cache-Control — no pre-render, images load on hover only 2026-04-24 23:03:49 +02:00
Pontoporeia e590d8e035 perf: pre-render student popover cards server-side into <template> tags — zero per-hover requests 2026-04-24 23:03:49 +02:00
Pontoporeia 53c3127140 feat: student name popover preview on /repertoire via htmx 2026-04-24 23:03:49 +02:00
Pontoporeia ede53746ba feat: student name popover preview on /repertoire via htmx 2026-04-24 23:03:49 +02:00
Pontoporeia d961f9533c feat: add objet field (tfe/thèse/frart) with share-link restriction and site-settings toggles 2026-04-24 23:03:49 +02:00
Pontoporeia dbaabaf8a0 merge all migrations into schema.sql 2026-04-24 23:03:49 +02:00
Pontoporeia 38031374c1 fix(partage): smtp view missing in schema + thanks redirect broken
- Add v_smtp_active VIEW to schema.sql (was only in migration 012,
  causing SmtpRelay::isConfigured() to always return false on fresh installs)
- Change thanks redirect from /partage/thanks.php to /partage/thanks
  (nginx 'location ~ \.php$ { deny all }' blocked the .php URL)
- Route /partage/thanks in index.php before slug validation
- Guard App::boot() in thanks.php to avoid double-boot when included
2026-04-24 23:03:49 +02:00
Pontoporeia 95bce2bbad Extract form CSS into form.css; scope system.css to system.php only 2026-04-24 23:03:49 +02:00
Pontoporeia d82556c596 Unify form CSS: move licence/share-badge styles to admin.css, remove inline style from partage form 2026-04-24 23:03:49 +02:00
Pontoporeia 057d2539eb SmtpRelay: parse EHLO caps, prefer AUTH PLAIN over AUTH LOGIN 2026-04-24 23:03:49 +02:00
Pontoporeia a3849a8e69 SmtpRelay: replace mail() stub with native socket SMTP client 2026-04-24 23:03:49 +02:00
Pontoporeia b448d0d40c Lock body scroll: only main/inner elements scroll on admin and public pages 2026-04-24 23:03:49 +02:00
Pontoporeia 5a58eefe66 feat(admin): add SMTP test email button on parametres page 2026-04-24 23:03:49 +02:00
Pontoporeia 4839b568de Separate admin views from controllers — move HTML to templates/admin/
All admin pages refactored to thin controllers + pure view templates, mirroring
the public-page pattern:

Controllers (public/admin/*.php): auth, data loading, include template
Views (templates/admin/*.php): pure HTML/PHP output
Fragment partials (templates/admin/partials/): toast, system-log-panel, system-nginx-config-panel

Pages migrated: login, tags, contenus, contenus-edit, account, acces-etudiante,
thanks, add, edit, parametres, system, index

Fragment endpoints refactored: system-fragment.php, toast-fragment.php
Skipped (pure redirects): logout, logs, status, import
2026-04-24 23:03:49 +02:00
Pontoporeia 362688c0fa fix: remove broken flash-messages include from admin footer; make repertoire columns scrollable 2026-04-24 23:03:49 +02:00
Pontoporeia 19ef2a11dc fix CSV importer AP/orientation name resolution + seed missing AP programs
- migration 014: adds Récits et expérimentation (RE), PACS, sets code NS
  on Narration Spéculative; applied to both posterg.db and test.db

- importer (admin/index.php): replaced the code-only ap_programs lookup
  (SELECT WHERE code=?) and the orientationMap short-code translation with
  two resolver closures that handle the real CSV format (full names):

  resolveAP(): alias map for L.I.E.N.S., case variants → exact name
  match → code match (legacy) → case-insensitive name match

  resolveOrientation(): legacy 2-letter code map → alias map for
  Installation/Performance, Arts numériques, Design numérique →
  exact name match → case-insensitive name match

  All 5 AP values and 13 orientation values from the real CSV now
  resolve to correct DB IDs. Legacy short-code CSVs (test.db format)
  continue to work unchanged.
2026-04-24 23:03:49 +02:00
Pontoporeia 73fdda4a7f fix repertoire AP/OR/FI columns and main scroll containment
- repertoire-index.php: add $colHasMatches per-column guard.
  Entries in a column are only faded when that column has at least one
  matched entry in the current result set. When a dimension has no
  matched entries (e.g. no thesis has orientation_id set yet), the
  entire column stays fully interactive — all values remain clickable.
  This fixes: empty columns, forced single-select, cascade fading.

- Database.php: revert allAp/allOr/allFi to full lookup-table queries
  so all known values are always shown (not just ones linked to theses).

- common.css: body is now a flex column; main gets flex:1 + min-height:0;
  header-search-wrap gets flex-shrink:0; duplicate html/body blocks merged.
- public.css: removed redundant top-level main block; home-main gets min-height:0.
- repertoire.css: search-main gets min-height:0 for proper flex scroll.
2026-04-24 23:03:49 +02:00
Pontoporeia 78449afe64 some css changes 2026-04-24 23:01:25 +02:00
Pontoporeia 1b02ccb1d5 fix: mark languages as required, add required-field visual indicators on both forms
- checkbox-list.php: support $required prop → adds required + aria-required on fieldset
- add.php: languages checkbox now marked required (matches server-side validation)
- partage/index.php: same for student form
- admin.css: dashed border on required inputs, bold labels, red asterisk via :has(), "Champs obligatoires" note
- Both forms now show "* Champs obligatoires" note at top

Server-side required fields = titre, auteurice, synopsis, année, orientation, ap, finality, languages (≥1), access_type_id, confirmation_email. All now have required attribute + visual asterisk.
2026-04-20 16:19:55 +02:00
Pontoporeia e21a4d81a2 refine: required confirmation_email field on both student forms, StudentEmail uses it directly
- Add dedicated 'confirmation_email' (type=email, required) field
  to student form at end of submission (partage + admin).
- ThesisCreateController now validates it is present and a valid
  email; form is rejected if missing/invalid.
- Autofocus mapping for confirmation_email errors.
- StudentEmail uses confirmation_email directly (removed extractEmail
  hack that mined email from free-form contact field).
2026-04-20 15:47:55 +02:00
Pontoporeia fa75ca4a65 fix: inline getDatabasePath into Database.php, delete config/config.php
- Remove require_once for config/config.php (file was never deployed — outside app/)
- Inline DB path resolution directly in Database::determineDatabasePath()
- Uses APP_ROOT when defined (bootstrap already loaded), falls back to __DIR__/../
- DB_ENV=test|prod env-var override preserved for tests
- php -S cli-server -> test.db, nginx/fpm -> posterg.db
2026-04-20 14:23:30 +02:00
Pontoporeia 468278349a fix: router.php — bypass admin requests to front controller 2026-04-20 14:13:44 +02:00
Pontoporeia 033599aa36 fix: config/config.php — correct DB paths to app/storage/, add test.db for dev 2026-04-20 14:12:26 +02:00
Pontoporeia 5af31aceda fix: Database.php require_once -> resolve config.php from app/src/ 2026-04-20 14:11:16 +02:00
Pontoporeia de2e7a61ee feat: single entry point routing — convert to front controller pattern
- Create app/public/index.php as front controller (bootstrap + Dispatcher)
- Rewrite app/router.php for PHP dev server → all non-asset requests to index.php
- Update Dispatcher to render full page layouts (head+header+view+footer)
- Move public view templates into templates/public/ (home, search, tfe, about, repertoire)
- Delete dead direct-access public/*.php files (apropos, search, tfe, licence, repertoire)
- Add clean URL routes to Dispatcher (/search, /tfe, /repertoire, /apropos, /licence, /media)
- Remove .php extensions from all internal links (header, views, templates, URLs)
- Update OG tags in controllers to use clean URLs
- Update nginx posterg.conf → front-controller try_files pattern, block direct .php access
- Update header.php and search-bar.php form actions to clean URLs
- Switch AboutController nav key from 'nav' to 'currentNav' for consistency
2026-04-20 12:42:15 +02:00
Pontoporeia 75f808bee4 feat: extract MediaController, wire into Dispatcher, delete media.php 2026-04-20 12:32:00 +02:00
Pontoporeia b03be51b92 feat: migrate admin system page to HTMX with tab-based navigation and log viewer 2026-04-16 15:03:40 +02:00
Pontoporeia 05002ccee4 fix: allow isAuthenticated() bypass in development mode 2026-04-16 15:03:33 +02:00
Pontoporeia bf30aab0b3 migrate apropos data from config/apropos.php to SQLite
- Create apropos_contents table via migration 010
- Add Database methods: getAproposContent(), saveAproposContent(), getAllAproposContents()
- Replace admin/pages.php with admin/contenus.php (renamed header from 'Pages statiques' to 'Contenus')
- Replace admin/pages-edit.php with admin/contenus-edit.php (support editing pages + apropos contents)
- Create admin/actions/apropos.php for saving apropos data (contacts, credits, erg_url)
- Update public/apropos.php to read contacts/credits/erg_url from DB
- Delete config/apropos.php
2026-04-16 14:22:41 +02:00
Pontoporeia 4158c72d08 admin: replace header 'Ajouter un TFE' nav link with toolbar button 2026-04-16 13:07:16 +02:00
Pontoporeia e70a65ffb6 fix: session boot on POST path, consolidate rate limiter via checkKey() 2026-04-16 13:07:16 +02:00
Pontoporeia a6df3c8c0e fix: /partage/<slug> routing (regex delimiter + nginx location) 2026-04-16 13:07:16 +02:00
Pontoporeia b7be93e30b Security: rate limiting and flash messaging for partage share links
- Add rate limiting (5 submissions per IP per 10 min, per share link)
  to prevent abuse of shared submission endpoints
- Replace all plain die() error responses with styled flash messages
  and redirects (invalid slug, disabled link, expired link, wrong password,
  rate limit exceeded, CSRF failure)
- Add dedicated error page renderer for disabled/expired links with
  home page link
- Password gate now uses flash message via session redirect instead
  of inline error variable
2026-04-16 13:07:16 +02:00
Pontoporeia 150099dc3c admin: replace header 'Ajouter un TFE' nav link with toolbar button 2026-04-16 13:07:16 +02:00
Pontoporeia c4705f6265 docs: add bookmarklet for auto-filling TFE test form
- bookmark.md with draggable link + readable source + lookup table reference
- all field selectors match actual form name attributes and schema IDs
2026-04-15 14:24:44 +02:00
Pontoporeia f4aba500e6 feat: student mode support for thanks page (admin-auth only)
- add hidden student_mode field in add.php form
- pass mode=student through redirect to thanks.php in formulaire.php
- thanks.php renders clean student thank-you page (no header, centered button)
- add CSS for .thanks-student-page, .btn-new-form, .thanks-success, .thanks-error
- admin auth always required; student mode is purely UI variant on the physical machine
2026-04-15 14:24:44 +02:00
Pontoporeia c3affd2285 admin/add: add ?mode=student toggle — hides admin header, keeps admin form css 2026-04-15 14:24:44 +02:00
Pontoporeia 150b5b1dac admin/add: add ?mode=student toggle — hides admin header, keeps auth 2026-04-15 14:24:44 +02:00
Pontoporeia 0eb2e310f4 admin/parametres: cleanup page — remove card syntax, use semantic HTML (checkboxes/fieldsets), move delete-all-TFE danger zone into maintenance 2026-04-15 14:24:44 +02:00
Pontoporeia fd4fb5ce4a Add delete/batch-delete and sortable columns to admin list
- Database: add deleteThesis() and bulkDeleteTheses() methods with file cleanup
- Database: add SORT_MAP + buildOrderBy() for safe column sorting
- Database: getThesesList() now respects sort/dir filter params
- New action: actions/delete.php (single + batch delete with CSRF)
- Admin index: delete button per row with confirmation dialog
- Admin index: batch 'Supprimer' button in bulk actions bar
- Admin index: sortable column headers (ID, Titre, Année, Orientation, AP, Statut)
- Admin index: sort state preserved in pagination links
- CSS: admin-btn-delete (red muted), admin-sort-link styles
2026-04-15 14:24:44 +02:00
Pontoporeia 1b104df51e Fix undefined $from– variable: brace-interpolate variables before en-dash in double-quoted string 2026-04-15 14:24:44 +02:00
Pontoporeia 6f04514aa2 fix: add structural guard for migration 008 in migrate.sh 2026-04-15 14:24:44 +02:00
Pontoporeia 0cb4451218 formulaire: default interne, unpublished, contact toggle, settings section 2026-04-15 14:24:44 +02:00
Pontoporeia 67a4aaac26 Fix nginx deduplication: remove nginx/scripts/ entirely, fix README typos and dead references 2026-04-15 14:24:44 +02:00
Pontoporeia 507f3eb704 Consolidate nginx docs and scripts, update paths 2026-04-15 14:24:44 +02:00
Pontoporeia 3cd96ed28a Deduplicate and standardise documentation
- Consolidate 36 markdown files → 14 (plus TODO.md)
- Merge overlapping docs into authoritative files:
  - database.md (from DATABASE_SPECIFICATION + QUICK_SCHEMA_REFERENCE + DATABASE_CONFIG + SETUP)
  - deployment.md (from SERVER_SETUP + COMPLETE_DEPLOYMENT_GUIDE + DEPLOYMENT_STEPS)
  - security.md (from SECURITY_ANALYSIS + TODO.SECURITY)
  - development.md (from DEVELOPMENT_GUIDE + LIVE_RELOAD_SETUP + TEST_CENTRALIZATION)
  - migration-history.md (from 11 past migration docs)
- Standardise all filenames to lowercase
- Remove non-doc files (Context.md research notes, chat export)
- Remove superseded docs (SECURITY.md pre-SQLite, SECURITY_IMPLEMENTATION, README_SECURE_SEARCH)
- Fix stale cross-references
2026-04-15 14:24:44 +02:00
Pontoporeia 0c29fa21e9 Prevent admin nav wrapping to match public header height 2026-04-09 14:51:43 +02:00
Pontoporeia c5c049eace Move public search bar below header so admin and public headers have same height 2026-04-09 14:37:49 +02:00
Pontoporeia 1885f2da92 Replace random HSL gradients in homepage cards with header-style gradient; header keeps its own CSS-variables gradient 2026-04-09 14:37:00 +02:00
Pontoporeia aa3fc50d92 use exact hard-coded gradient on header and card placeholders 2026-04-09 14:34:59 +02:00
Pontoporeia a13e73aed3 Replace random HSL gradients in homepage cards with header gradient 2026-04-09 14:29:17 +02:00
Pontoporeia 07f0afde25 cache-bust: add filemtime-based versioning to all CSS and JS assets 2026-04-09 14:08:19 +02:00
Pontoporeia 424f79c819 typography: switch display font from Combined to Ductus 2026-04-08 18:09:35 +02:00
Pontoporeia a333a5fdad Rebrand: replace PostErg with XAMXAM in admin header link, default title, and OG site_name 2026-04-08 18:04:35 +02:00
Pontoporeia 18045af243 favicon: replace SVG placeholder with full PNG/ICO set from assets/favicon/ 2026-04-08 18:01:41 +02:00
Pontoporeia d68645f1b1 style: retheme system page to site light palette — remove dark --sys-* tokens 2026-04-08 17:49:50 +02:00
Pontoporeia df414346e9 fix: SystemController php-fpm detection — probe phpX.Y-fpm from running PHP version first 2026-04-08 17:46:42 +02:00
Pontoporeia 7117934d07 fix: replace mb_strlen/mb_substr with strlen/substr — mbstring not available on prod 2026-04-08 17:42:01 +02:00
Pontoporeia 9eec5d3ac0 SPECS.md 2026-04-08 16:03:31 +02:00
Pontoporeia 49b113319a Add AP filter to admin list; fix reset as unstyled button 2026-04-08 15:22:39 +02:00
Pontoporeia 4199b206db Move CSV import to inline dialog on list page 2026-04-08 15:16:10 +02:00
Pontoporeia 603af07b68 Add Paramètres page: consolidate maintenance + account settings 2026-04-08 15:06:51 +02:00
Pontoporeia ba135f0cb5 fix: replace 'Xamxam' with 'Posterg' in public nav header 2026-04-08 14:14:37 +02:00
Pontoporeia f6977384b9 migrate to utopia fluid type and space scales across all CSS 2026-04-08 14:14:37 +02:00
Pontoporeia ad06bbbcaf bump all font-size values ~10% across all CSS files 2026-04-08 14:14:37 +02:00
Pontoporeia 0c2276d5ad Split search into search.php; repertoire.php is index-only 2026-04-08 14:14:37 +02:00
Pontoporeia e96ec572be tfe: hyperlink metadata values to repertoire.php with correct filter params 2026-04-08 14:14:37 +02:00
Pontoporeia 55c6ac21b8 fix tfe page: scope common.css header to body>header, fix grid width collapse, remove overflow-y clip 2026-04-08 14:14:37 +02:00
Pontoporeia 3a1cd5b43e tfe page: author above title, interne/externe jury split, rounded images, strip contact protocol 2026-04-08 14:14:37 +02:00
Pontoporeia 547d581e26 Removed footer navbar 2026-04-08 14:14:37 +02:00
Pontoporeia 11a665e096 Improve À propos page layout: sticky TOC nav, bordered contact rows, credits dl grid 2026-04-08 14:14:37 +02:00
Pontoporeia dddfc8554b fonts: add --font-body/--font-display vars, use them everywhere 2026-04-08 14:14:37 +02:00
Pontoporeia 797eaf87d1 Apply correct fonts: Combined for titles, BBBDMSans for body text 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 8b27acec27 fix homepage card grid styling: gaps, borders, rounded corners, gradient aspect-ratio 2026-04-08 14:14:37 +02:00
Pontoporeia 0bfb24723f fix(font): rebuild Combinedd.otf with sorted kern pairs to fix browser discard 2026-04-08 14:14:37 +02:00
Pontoporeia b8e94f1b6b fix(css): add format hint + descriptors to @font-face for police1/Combinedd.otf 2026-04-08 14:14:37 +02:00
Pontoporeia b45e6c50cc fix: admin CSP allow inline scripts
script-src 'self' 'unsafe-inline' added to admin Content-Security-Policy.
default-src 'self' was blocking OverType editor init block and
the dev live-reload poller. Admin section is auth-gated so
unsafe-inline is acceptable.
2026-04-08 14:14:37 +02:00
Pontoporeia e6960f0c9c fix: RateLimit permission denied — code + deploy scripts
RateLimit.php:
- Silence mkdir() with @ operator
- Guard file_put_contents with is_writable() check (graceful degrade)

scripts/deploy-server.sh + setup-server.sh:
- mkdir -p storage/cache/rate_limit on every deploy
- chown www-data:posterg + chmod 2775 on storage/cache/
  so php-fpm can always write rate limit files
2026-04-06 16:45:14 +02:00
Pontoporeia 756ddb5765 fix: RateLimit graceful degradation on permission denied
Silence mkdir() with @ operator; guard file_put_contents with
is_writable() check. When storage/cache/rate_limit is not writable
by php-fpm, requests are allowed through instead of throwing
warnings that flood the nginx error log.
2026-04-06 16:40:55 +02:00
Pontoporeia 6a1b41ac93 css: remove dark mode, unify token system, eliminate all hardcoded colors
- Remove @media (prefers-color-scheme: dark) block from variables.css
- Delete colors.css (dead reference doc, never loaded)
- Add semantic tokens to variables.css:
    --header-gradient-fade, --header-shadow-strong/soft, --header-nav-active-border
    --search-error-bg/border/color
    --sys-bg-surface/deep/panel, --sys-border/border-deep/border-hover
    --sys-text-dim/bright/body, --sys-overlay, --sys-syntax-* (7 highlight tokens)
    --success/warning/error/blue/yellow/green -muted-bg/border/hover alpha overlays
    --danger-border-muted
- Replace all hardcoded hex/rgba in common.css, main.css, search.css, admin.css, system.css
- Fix --border-color typo -> --border-primary in search.css
- Fix view-toggle__btn active color: --text-primary -> --accent-foreground
- Admin and public share identical token set, no separate admin theme
- Update README.md
2026-04-06 16:16:11 +02:00
Pontoporeia e73fcfd0c8 fix: drop curl_close() call (deprecated PHP 8.5, no-op since 8.0) 2026-04-06 16:11:34 +02:00
Pontoporeia 9a9dfd2b9e fix(apropos): replace straight apostrophe in l'ERG role string (parse error) 2026-04-06 15:34:06 +02:00
Pontoporeia f6e2c77d1a Adjusting admin static pages edit page 2026-04-06 15:33:26 +02:00
Pontoporeia 480451aa2b replace EasyMDE (333KB) with OverType (118KB) in pages-edit.php
- Remove easymde.min.js (320KB) and easymde.min.css (13KB)
- Vendor overtype.min.js (118KB, v2.3.5)
- Replace <textarea name=content> + 60-line toolbar/SVG init with:
  - <input type=hidden name=content> for form submission
  - <div id=editor> as OverType mount target
  - 6-line init: value from hidden input, onChange syncs it back
- Net saving: ~215KB assets, ~54 lines of inline JS
2026-04-06 15:33:08 +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 a60e742d1a Extract last 3 inline styles from admin templates into CSS classes
admin/thanks.php:
- <div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap;"> → class="admin-action-bar"
- <p style="color:var(--text-secondary);"> → class="admin-muted"

admin/pages.php:
- Éditer button style="font-size:.8rem;padding:.3rem .75rem;" → class="admin-btn admin-btn--sm"

admin.css (Thesis info sections block):
- Added .admin-action-bar { margin-top:1.5rem; display:flex; gap:0.75rem; flex-wrap:wrap }
- Added .admin-muted { color: var(--text-secondary) }

The only remaining inline style in any admin PHP file is the dynamic
--disk-pct/--disk-color custom properties on the disk bar in system.php,
which carry PHP runtime values and cannot be moved to static CSS.
2026-04-06 15:33:08 +02:00
Pontoporeia ca8081575c Add prefers-color-scheme dark mode for public pages
Scope: variables.css, search.css, todo/04-accessibility.md

- variables.css: add @media (prefers-color-scheme: dark) block scoped to
  body:not(.admin-body); overrides all semantic tokens with dark equivalents:
  --bg-* (#111→#333 range), --text-* (#eee/aaa/777),
  --border-* (#333/#444), --accent-primary lightened to #b87fd4
  (4.5:1 contrast on #111 background), --accent-secondary stays #9557b5,
  --accent-foreground flipped to #111111 for dark buttons,
  --accent-muted adjusted to rgba(184,127,212,0.15),
  status colours muted for dark (success #4db886, error #e05555,
  warning #d4a830); new --search-error-{bg,border,color} tokens added
  to :root (light: #fff0f0/#c00) and overridden in dark (#2a1515/#e05555)

- search.css: replace three hardcoded hex values in .search-error rule
  with var(--search-error-bg/border/color) so dark mode applies cleanly

- Admin pages are entirely unaffected: .admin-body body class is excluded
  from the dark-mode selector; system.css already has its own dark palette
2026-04-06 15:33:08 +02:00
Pontoporeia 2841e05716 Extract ThesisCreateController; add Database publish methods
Consolidate action handlers into controller methods (todo/02-php-components.md).

src/ThesisCreateController.php (new, 435 lines)
  Mirrors ThesisEditController for the add-thesis flow.

  make()           — factory; instantiates Database via new Database()
  loadFormData()   — returns all lookup tables needed by admin/add.php
                     (orientations, apPrograms, finalityTypes, languages,
                      formatTypes, licenseTypes)
  submit(post, files) — full new-thesis creation pipeline:
    1. validateAndSanitise() — trims/strips HTML, validates required fields,
       year range, orientation/ap/finality IDs, language selection, max-10
       keywords, URL format; throws named Exception on failure
    2. findOrCreateAuthor() — reuses existing DB method
    3. Transaction: createThesis + setThesisJury + setThesisLanguages +
       setThesisFormats + setThesisTags; rolls back on any failure
    4. File uploads outside transaction: cover image (JPG/PNG only, stored in
       storage/covers/), banner via handleBannerUpload(), thesis files
       (PDF/JPG/PNG/MP4/ZIP/VTT, stored in storage/theses/YEAR/IDENT/,
       file_type auto-detected: caption/annex/main/other)
  autofocusFieldForError() — static; maps exception messages to field names
    for WCAG 3.3.1 autofocus on re-render (same contract as
    ThesisEditController::autofocusFieldForError)

admin/actions/formulaire.php  346 → 45 lines
  Now: bootstrap + CSRF guard + ThesisCreateController::make()->submit() +
  flash/redirect on error. All validation, DB logic, and file handling removed.

admin/add.php
  Lookup-table block (new Database() + 6 individual DB calls) replaced with
  ThesisCreateController::make()->loadFormData() + extract().

src/Database.php — two new methods added
  setPublished(int , bool ): void
    UPDATE theses SET is_published = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
  bulkSetPublished(int[] , bool ): void
    Same but with an IN (...) clause for multiple IDs

admin/actions/publish.php  100 → 65 lines
  Raw SQL (->prepare('UPDATE theses SET is_published = ?...')) replaced
  with ->setPublished() / ->bulkSetPublished(). No raw PDO calls remain
  in any action handler file.
2026-04-06 15:33:08 +02:00
Pontoporeia b1e70a2bf1 Extract HomeController from public/index.php
Move all data-fetching and view-variable assembly out of public/index.php
into a new src/HomeController.php, following the same pattern as
SearchController, TfeController, SystemController, and ThesisEditController.

HomeController::create() builds the Database singleton dependency.
HomeController::handle() encapsulates:
- GET param parsing (page, year) with safe type coercion
- Display-mode detection: default random-latest view / year-filtered /
  paginated-all theses
- All DB calls: getLatestPublishedYear, getLatestYearTheses, searchTheses,
  countSearchResults, getPublishedTheses, countPublishedTheses,
  getCoverPathsForTheses, getAvailableYears
- Batch cover-image loading for theses without a banner_path
- baseParams assembly for the pagination partial
- OG / meta tag array construction
- Graceful error handling (logs exception, returns safe empty state)
- Returns a flat array of view variables

public/index.php is now a 6-line dispatcher (require + create + handle +
extract) followed by a pure view template. Reduced from 100 to 71 lines.
All error-handling and data logic removed from the view layer entirely.
2026-04-06 15:33:08 +02:00
Pontoporeia 89067a521f Extract TfeController from public/tfe.php
src/TfeController.php (new, 195 lines):
- Dedicated controller for the public TFE detail page
- create(): Database singleton injection, ready-to-use factory
- handle(): validates id param (redirect to index.php on missing/invalid/404),
  loads thesis via getThesisById(), fetches access type via getThesisAccessTypeId()
- buildMetaDescription(): strip_tags + 160-char mb_substr truncation
- resolveOgImage(): banner_path → first image file → empty string resolution
- buildOgTags(): full og:type/title/description/url/image/image_alt/site_name +
  article:author / article:published_time assembly
- collectCaptionPaths(): ordered list of VTT paths for N-th-video pairing
- returns flat array of all view variables including ogTags, captionFiles,
  pageTitle, metaDescription, isInterdit, bodyClass, extraCss, currentNav

public/tfe.php (271 → 206 lines):
- Reduced to 9-line dispatcher: require TfeController, create(), handle(), extract()
- $db reference removed from view layer entirely
- Inline OG tag block (~20 lines) removed
- Inline meta-description block (~5 lines) removed
- Inline caption-collection loop (~10 lines) removed
- $captionFiles replaces $_captionFiles in the video pairing section

todo/02-php-components.md:
- TfeController extraction marked done
- 'Move OG tag construction into controller logic' marked done
- Remaining item narrowed to public/index.php home-page controller
2026-04-06 15:33:08 +02:00
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