Commit Graph

55 Commits

Author SHA1 Message Date
Pontoporeia
150099dc3c admin: replace header 'Ajouter un TFE' nav link with toolbar button 2026-04-16 13:07:16 +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
0cb4451218 formulaire: default interne, unpublished, contact toggle, settings section 2026-04-15 14:24:44 +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
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
49b113319a Add AP filter to admin list; fix reset as unstyled button 2026-04-08 15:22:39 +02:00
Pontoporeia
0c2276d5ad Split search into search.php; repertoire.php is index-only 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
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
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
e73fcfd0c8 fix: drop curl_close() call (deprecated PHP 8.5, no-op since 8.0) 2026-04-06 16:11:34 +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
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
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
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
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
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
a88e5562f8 fix(config): auto-route test.db locally, posterg.db on production
- config.php: getDatabasePath() detects php built-in CLI server
  (php_sapi_name() === 'cli-server') and routes to test.db; all
  other SAPIs (nginx/fpm) get posterg.db. DB_ENV env-var still
  overrides either way.

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

- justfile: serve still calls migrate (which now handles init too),
  no DB_ENV prefix needed since sapi detection handles routing.
2026-04-01 15:55:12 +02:00
Pontoporeia
7a4a471838 fix: search filter labels, 429 page styling, __wakeup PHP 8.x deprecation
- Replace three <span class='search-filter-label'> with proper <label for='...'> elements in
  search.php filter bar; add id attributes to the corresponding <select> elements so the
  label/control association is programmatic (WCAG 1.3.1, 3.3.2).

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

- Fix PHP 8.x __wakeup() deprecation in Database.php singleton guard: replace the throw
  statement with trigger_error(..., E_USER_ERROR) and add an explicit void return type
  (Refactor audit C).
2026-03-29 15:47:30 +02:00
Pontoporeia
126703f340 tfe.php: full semantic HTML overhaul
- Replace <div class="tfe-layout"> with <article>, <div class="tfe-left"> with
  <header>, <div class="tfe-right"> with <aside> (supplementary media column)
- Fix inverted heading hierarchy: <h1> is now the thesis title (primary topic);
  author demoted to <p class="tfe-author"> (metadata, not a heading)
- Replace <div class="tfe-meta-list"> / <div class="tfe-meta-item"> / <span class="label">
  / <span class="value"> with <dl> / <dt> / <dd> (WCAG 1.3.1 info & relationships)
- Replace <div class="tfe-media-block"> with <figure>; <p class="tfe-file-caption">
  with <figcaption>; PDF <embed> gets .tfe-pdf-fallback download link (WCAG 4.1.2)
- Move back link to top of left column; extract inline styles to .tfe-back-link,
  .tfe-note-value, .tfe-restricted CSS classes
- Fix image alt text: description column used when populated, fallback to
  "Title — Author" instead of raw filename (WCAG 1.1.1)
- Add sr-only new-tab warning on baiu_link (WCAG 1.3.1 / 2.4.4)
- Fix PDF embed height: clamp(300px, 80vh, 700px) prevents horizontal overflow
  on small screens (WCAG 1.4.10 reflow)
- tfe.css: update all selectors to match new structure; remove inline styles;
  unify .tfe-restricted and .tfe-no-files; add .tfe-pdf-fallback, .tfe-back-link
2026-03-28 19:12:01 +01:00
Pontoporeia
4f5ff5a22c refactor: extract edit.php POST handler to actions/edit.php
edit.php was a 530-line file mixing form display, POST handling, file
uploads, and reference-data loading. This refactor splits it along the
same action-file pattern already used by formulaire.php, tag.php, and
page.php.

Changes:
- public/admin/actions/edit.php (new): standalone POST handler; auth
  guard, CSRF check, transaction, redirect with session flash messages
- public/admin/edit.php: display-only; reads edit_success/edit_error
  flash keys from session; form action points to actions/edit.php via
  a hidden thesis_id field instead of a query-string self-post
- src/Database.php: four new methods to remove all raw PDO from both
  files:
    - updateThesis(int, array): void  — UPDATE theses core fields
    - setThesisAuthors(int, array): void  — delete-then-reinsert authors
    - getThesisLanguageIds(int): array — SELECT language_id for form
    - getThesisFormatIds(int): array   — SELECT format_id for form
2026-03-28 18:08:23 +01:00
Pontoporeia
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
61ac3c002d refactor: encapsulate thesis creation SQL in Database::createThesis()
Move the raw identifier-generation query and the INSERT INTO theses /
INSERT INTO thesis_authors statements out of formulaire.php into two new
Database methods:

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

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

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

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

All four test suites (Unit, RateLimit, Integration, Security) pass.
2026-03-28 13:52:43 +01:00
Pontoporeia
e126e1a3b0 refactor: use encapsulated Database methods in formulaire.php and edit.php 2026-03-28 13:49:51 +01:00
Pontoporeia
71167b2cdf fix: remove DB_ENV auto-detection; require explicit DB_ENV=test for tests
src/config.php: remove the file-existence fallback that silently redirected
all requests to test.db whenever that file was present on disk. getDatabasePath()
now always returns the production DB unless DB_ENV=test is explicitly set.

tests/run-tests.php: putenv('DB_ENV=test') at the top so the suite always
targets test.db regardless of what is set in the shell environment.

tests/Unit/DatabaseTest.php, tests/Integration/SearchTest.php,
tests/Security/SecurityTest.php: same putenv() guard added to each file so
they work correctly when run standalone (e.g. just test-unit).

justfile: all test and DB-development recipes now prefix DB_ENV=test to their
php/sqlite3 commands, making the intent explicit in the recipe itself.

Fixes: a developer who ran the test suite and kept test.db on disk would
silently hit test data when browsing the local site with no DB_ENV set.
2026-03-28 13:43:04 +01:00
Pontoporeia
7d96a08324 perf: replace fat-view student index query with lean getPublishedAuthors()
The répertoire page was loading the full v_theses_public view
(15 JOINs + 8 GROUP_CONCAT temp B-trees) via getAllPublishedTheses()
just to build the student name → thesis-id map on the index page.
Only two columns (id, authors) were ever consumed by the template.

Add Database::getPublishedAuthors(): array
- Queries thesis_authors JOIN authors directly on the theses base table
- Filters on theses.is_published = 1 using the existing index
- Returns only id + GROUP_CONCAT(authors) — no view expansion
- Results verified identical to the old getAllPublishedTheses() output

Update search.php to call getPublishedAuthors() instead.
Mark getAllPublishedTheses() @deprecated in Database.php.

All tests pass.
2026-03-28 13:35:43 +01:00
Pontoporeia
1181cfa88b encapsulate raw PDO queries leaking from callers into Database.php methods
- Add getThesisAccessTypeId(int $id): ?int — replaces raw SELECT in tfe.php
- Add getCoverPathsForTheses(array $ids): array — replaces raw SELECT/IN query in index.php
- Add getFileVisibility(string $path): ?int — replaces raw join query in media.php
- Add getThesisBannerPath(int $id): ?string — replaces unparameterised SQL injection in
  edit.php (SELECT banner_path FROM theses WHERE id = $thesisId was interpolating $thesisId
  directly into the query string; now parameterised via prepared statement)
- Add getThesisRawFields(int $id): ?array — replaces raw SELECT license_id/access_type_id/
  context_note in edit.php
- Add getThesisCount(): int — replaces raw SELECT COUNT(*) in system.php

Callers updated: public/tfe.php, public/index.php, public/media.php,
public/admin/edit.php, public/admin/system.php
2026-03-28 13:32:34 +01:00
Pontoporeia
20e5f71634 Fix two backend correctness issues
- Wrap setThesisJury() in a transaction: the method did a DELETE then multiple
  INSERTs with no atomicity guarantee. A partial failure (e.g. findOrCreateSupervisor
  throwing) would leave the jury table with orphaned rows. The fix uses
  pdo->inTransaction() to avoid nesting when called from within an outer transaction,
  and performs beginTransaction/commit/rollBack otherwise.

- Replace raw PDO query in admin/thanks.php with db->getThesisFiles(): the file
  listing after TFE submission was manually preparing a SELECT on thesis_files
  instead of calling the existing Database::getThesisFiles() method. Removes the
  getPDO() call entirely from that file.
2026-03-28 13:28:24 +01:00
Pontoporeia
69e161ada3 fix(admin): stats bar always shows whole-DB counts, not filtered counts
admin/index.php showed "TFE total / Publiés / En attente" by running
array_filter() over the already-filtered $theses array returned by
getThesesList(). When any search or year filter was active the three
numbers reflected only the matching subset, making the stats misleading
(e.g. searching for a single student would show "1 total, 0 publiés").

Add Database::getThesesStats(): array — a single SQL aggregation query:
  SELECT COUNT(*), SUM(is_published), SUM(NOT is_published) FROM theses

This runs against the raw theses table with no filters, so the counters
always display the true whole-database figures regardless of what filter
the admin has active. admin/index.php now calls getThesesStats() and
reads $stats['total'], $stats['published'], $stats['pending'] instead
of the array_filter expressions.
2026-03-28 11:42:44 +01:00
Pontoporeia
2e277b104e refactor(Database): remove dead CRUD helpers and alias proliferation
Remove 5 unused ID-lookup helpers (getOrientationId, getAPProgramId,
getFinalityId, getLanguageId, getFormatId) — forms have always passed
FK ids directly from <select> elements; these methods were never called
outside import.php, which now uses inline PDO queries instead.

Collapse 13 alias methods down to the single canonical name for each:
  getAllOrientations, getAllAPPrograms, getAllFinalityTypes,
  getAllFormatTypes, getAllLanguages, getAllLicenseTypes,
  getUsedTags, findOrCreateTag

The short-name variants (getOrientations, getApPrograms, etc.) and
compat aliases (getUsedKeywords, findOrCreateKeyword, getAllLicenseTypes
delegating to getLicenseTypes) are deleted. All call-sites updated:
  - public/search.php: getOrientations→getAllOrientations, etc.
  - public/admin/import.php: findOrCreateKeyword→findOrCreateTag,
    thesis_keywords→thesis_tags, keyword_id→tag_id (fixes stale table
    reference from pre-migration-001 that bypassed the M2M rename)
  - tests/Unit/DatabaseTest.php: remove alias smoke-test (test 7)

Database.php: 948 → 848 lines (-100).
2026-03-28 11:35:23 +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
b12ae73e91 tests: fix SecurityTest fatal TypeError — update searchTheses call to use array params
SecurityTest::Test1 was calling $db->searchTheses($string) with a plain
string, but searchTheses() was refactored to require array $params when
the tag M2M work landed.  This caused an immediate PHP fatal TypeError
before any SQL ever ran, killing the entire Security test suite with
exit code 255 and masking all three tests.

Fix: pass each malicious payload via ['query' => $string] which is the
correct API and properly exercises the parameterised query path through
validateSearchParams() + buildSearchConditions().  Added a clarifying
comment explaining why the array form is required.

All 4 test suites now pass:
  - Database (Unit):   7/7
  - Rate Limit (Unit): 5/5
  - Search (Integration): 6/6
  - Security:          3/3
2026-03-26 18:54:20 +01:00
Pontoporeia
372abb5cd6 feat: tag management tests, maintenance mode polish, répertoire pagination fix
- tests/Unit/DatabaseTest.php: tests 5-7 for findOrCreateTag round-trip, getUsedTags column, alias
- tests/Integration/SearchTest.php: tests 4-6 for tag subquery, full-text query, count consistency
- Database: getAllPublishedTheses() bypasses 100-row search cap for student index
- search.php: uses getAllPublishedTheses() for étudiantes column; all tests pass
2026-03-24 15:38:36 +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
0933137540 refactor: rename keywords→tags M2M (migration 001)
- migration 001_rename_keywords_to_tags.sql: CREATE tags/thesis_tags from keywords/thesis_keywords,
  copy data, drop old tables, rebuild indexes and views
- schema.sql: tags table, thesis_tags junction, updated indexes and v_theses_full/v_theses_public
- Database.php: findOrCreateTag(), getUsedTags() with proper JOIN; backwards-compat aliases;
  buildSearchConditions uses EXISTS subquery on thesis_tags+tags with vp. alias throughout
- admin/actions/formulaire.php: INSERT OR IGNORE INTO thesis_tags
- admin/edit.php: DELETE FROM thesis_tags + findOrCreateTag
- search.php: $kw['name'] (was $kw['keyword'])
- fixtures/CreateTestDatabase.php: tags/thesis_tags table names
2026-03-24 13:30:53 +01:00
Pontoporeia
cefceb046c feat: jury composition + banner image upload
- migration 004: thesis_supervisors.role + is_external; view adds jury_president/jury_promoteurs/jury_lecteurs
- migration 005: theses.banner_path; view exposes t.banner_path and t.license_id
- Database: getThesisJury(), setThesisJury(), setBannerPath()
- admin/add.php: jury fieldset (président/promoteur/lecteurs + externe checkboxes, JS add/remove rows); banner file input
- admin/edit.php: jury fieldset pre-populated from DB; banner preview + remove checkbox + upload; multipart form
- admin/actions/formulaire.php: parse jury fields → setThesisJury(); banner upload to banners/
- tfe.php: three conditional jury rows (président·e, promoteur·ice, lecteur·ices)
- schema.sql: updated thesis_supervisors, theses, v_theses_full, v_theses_public definitions
- admin.css: fieldset, jury-row, jury-entry, btn-remove styles
2026-03-24 13:25:23 +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
52978aa658 ops: simplify justfile, guard deploy-db, extract scripts, fix .gitignore 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
eaad740574 refactor: extract buildSearchConditions, add getThesesList, remove dead code, fix SearchTest
- Database: extract private buildSearchConditions(array $params): array shared by
  searchTheses() and countSearchResults(), eliminating ~80 lines of duplication;
  add array type hints to both public methods
- Database: add getThesesList(array $filters) and getAllYears() so admin/index.php
  no longer builds raw SQL inline
- admin/index.php: replace inline PDO query block with $db->getThesesList() /
  $db->getAllYears(); drop the now-unused $pdo local
- config/bootstrap.php: remove dead include_template() helper and the
  vendor/autoload.php Composer stub (no vendor/ directory exists)
- apps/: delete entire directory (leftover artefact, no code references it)
- tests/Integration/SearchTest.php: fix three searchTheses() calls from bare
  strings to proper array params to match the method signature (prevented TypeError)
2026-02-24 23:21:44 +01:00
Théophile Gervreau-Mercier
73c27a067d Make search page header more compact and fix layout structure
- Reduce all spacing and padding in header for more compact fit
- Fix back button overflow by removing width: 100% and adding overflow handling
- Make filter section more compact with smaller fonts and spacing
- Add main-wrapper div to group main and footer
- Keep rounded corners (40px) on all three sections like main.css
- Footer stays at bottom of main content area
- Fix HTML structure: footer outside main, both inside wrapper
2026-02-12 13:41:17 +01:00
Théophile Gervreau-Mercier
bc98df4993 Improve search page with denser header and filter layout
- Transform header into compact search bar with back button
- Move filters panel underneath search bar (collapsible)
- Display results in grid layout matching main.css style
- Add pagination controls in main section
- Show result count in footer
- Prevent overflow with responsive design and proper flex constraints
- Reduce padding and font sizes for denser layout
2026-02-12 13:22:09 +01:00