- public/admin/logs.php: new page tailing nginx error/access + PHP-FPM logs. Selector for log file and line count (50/100/200/500, default 100). Lines reversed (newest first), colour-coded by severity, numbered gutter. Graceful degradation when exec() unavailable or file unreadable (dev msg). - templates/admin/head.php: 'Journaux' nav link added after 'Statut'. - public/admin/status.php: remove curl_close() call deprecated in PHP 8.5 (no-op since PHP 8.0); replace with unset($ch) to silence the warning that was leaking raw text above the page output.
14 KiB
TODO
Styling Redesign (matching design images)
- Redesign shared nav bar (purple gradient top, flat, POSTERG / RÉPERTOIRE / À PROPOS)
- Redesign shared search bar (full-width, icon, bottom border only, white bg)
- Rewrite
common.css(nav + search bar components) - Rewrite
main.css(home page — white bg, media card grid, label below) - Rewrite
search.css(répertoire index — 4-col ANNÉES/CATÉGORIES/ÉTUDIANTES/MOTS-CLÉS) - Rewrite
tfe.css(TFE page — 2-col, large author/title left, media right) - Add
apropos.css(À Propos — 2-col, large monospace text) - Rewrite
admin.css(dark bg, purple gradient nav, bottom-border-only form inputs) - Update
templates/nav.php(new shared nav partial) - Update
templates/search-bar.php(new shared search bar partial) - Rewrite
public/index.php(home page with new layout) - Rewrite
public/search.php(répertoire index view + search results view) - Rewrite
public/tfe.php(individual TFE page) - Create
public/apropos.php(À Propos page) - Rewrite
templates/admin/head.php(admin nav) - Rewrite
templates/admin/footer.php(clean close) - Rewrite
public/admin/add.php(form with row layout) - Rewrite
public/admin/index.php(dark table) - Rewrite
public/admin/edit.php(form with row layout) - Rewrite
public/admin/login.php(centered dark login box) - Rewrite
public/admin/thanks.php(dark info cards) - Rewrite
public/admin/import.php(clean dark form)
Justfile / Ops
- Simplify
serveanddeployto one recipe each - Remove sysadmin recipes (server-logs, server-status, deploy-nginx, deploy-admin-tools)
- Extract server scripts to
scripts/(deploy-server.sh, manage-admin-users.sh) - Guard
deploy-dbagainst overwriting existing remote database - Update README.md and docs/SERVER_SETUP.md to reflect current structure
NEW FEATURES
1 — License page (public)
Create a public-facing /licence.php page, styled consistently with apropos.php.
public/licence.php— new public page; fetches content frompagestable (slug'licenses'); renders with Parsedown Markdown; usesapropos.csslayouttemplates/nav.php— add "Licence" link between "Répertoire" and "À Propos"; applysite-nav__link--activewhen$currentNav === 'licence'- The
pagestable row for slug'licenses'verified in live DB
2 — Admin: WYSIWYG/Markdown editors for static pages
Allow admins to edit the content of the "À propos" and "Licence" pages from the admin
panel, stored in the existing pages table.
2a — src/Database.php
getPage(string $slug): array|null—SELECT * FROM pages WHERE slug = ?savePage(string $slug, string $content): void— throws if slug not foundgetAllPages(): array— for listing in admin
2b — Admin pages editor UI
public/admin/pages.php— list all editable pages; links to edit each onepublic/admin/pages-edit.php— EasyMDE WYSIWYG Markdown editor via CDN
2c — public/admin/actions/page.php
- Auth guard + CSRF check + slug validation + length validation + savePage + redirect
2d — Public pages render Markdown
public/apropos.php— renders$db->getPage('about')via Parsedown (bundledsrc/Parsedown.php)public/licence.php— renders$db->getPage('licenses')via Parsedown- Parsedown bundled as
src/Parsedown.php(zero-dependency, MIT)
2e — Nav links in admin
templates/admin/head.php— "Pages statiques" nav item added
3 — License field on TFE forms
Add a "Licence" dropdown to the Add and Edit TFE forms. The license_types table
already exists in the schema with an id, name, description structure but has
no seed data yet.
3a — Schema / DB
storage/schema.sql— seedINSERT OR IGNOREfor 8 CC licence types addedstorage/migrations/003_seed_license_types.sql— migration created + applied- Verified live DB has
license_typeswith 8 rows
3b — src/Database.php
getLicenseTypes(): arraygetAllLicenseTypes(): array— alias
3c — Add form (public/admin/add.php)
- Loads
$licenseTypes; "Licence"<select name="license_id">added before duration
3d — Add action (public/admin/actions/formulaire.php)
$licenseIdparsed + included in INSERT
3e — Edit form (public/admin/edit.php)
- Loads
$licenseTypes; rawlicense_idFK fetched directly; select pre-populated - POST handler:
license_idincluded in UPDATE
3f — View update
storage/schema.sql—v_theses_fullnow exposest.license_idraw FK (done as part of 004/005 view rebuild)
3g — TFE public page
public/tfe.php— "Licence :" meta row added, shown when non-null
4 — Jury composition section in Add/Edit forms
Replace the current flat "promoteur interne / externe" fields with a structured Composition du jury section: président·e, promoteur·ice, lecteur·ices (mixed internal/external).
4a — Schema / DB
Current state: supervisors table + thesis_supervisors junction with a bare
supervisor_order integer. No role distinction.
storage/migrations/004_jury_roles.sql: -ALTER TABLE thesis_supervisors ADD COLUMN role TEXT NOT NULL DEFAULT 'promoteur'— role values:'president','promoteur','lecteur'-ALTER TABLE thesis_supervisors ADD COLUMN is_external INTEGER NOT NULL DEFAULT 0— 1 = external, 0 = internal (replaces the old "promoteur externe" free-text field) - No data loss: existing rows getrole = 'promoteur',is_external = 0storage/schema.sql— add the two new columns tothesis_supervisorsdefinition; updatev_theses_fullto expose jury members grouped by role: -GROUP_CONCAT(DISTINCT CASE WHEN ts.role='president' THEN s.name END) as jury_president-GROUP_CONCAT(DISTINCT CASE WHEN ts.role='promoteur' THEN s.name END) as jury_promoteurs-GROUP_CONCAT(DISTINCT CASE WHEN ts.role='lecteur' THEN s.name END) as jury_lecteurs- Keep the existingsupervisorscolumn (all names) for backwards compat - Migration SQL must DROP + CREATE the view
4b — src/Database.php
getThesisJury(int $thesisId): array— fetch all supervisors for a thesis with their role and is_external flagsetThesisJury(int $thesisId, array $juryMembers): void— delete + re-insert
4c — Add form (public/admin/add.php)
- Remove
promoteuriceandpromoteurice_externefields - Add "Composition du jury" fieldset: président·e, promoteur·ice + externe checkbox, dynamic lecteur·ices list with JS add/remove
4d — Add action (public/admin/actions/formulaire.php)
- Removed old promoteurice parsing; parse jury fields; call
$db->setThesisJury()
4e — Edit form (public/admin/edit.php)
- Load
$jury = $db->getThesisJury($thesisId); jury fieldset pre-populated from DB - POST handler calls
$db->setThesisJury()
4f — TFE public page (public/tfe.php)
- Three conditional jury rows: Président·e, Promoteur·ice, Lecteur·ices
5 — Banner image upload for home page cards
Each TFE can have an optional banner image used as its home page card thumbnail.
This is distinct from the existing "couverture" file concept (which goes into
thesis_files as type 'cover') — the banner is specifically optimised for the
home grid (wider, shorter aspect ratio).
5a — Schema / DB
storage/migrations/005_add_banner.sql—ALTER TABLE theses ADD COLUMN banner_path TEXTstorage/schema.sql—banner_path TEXTintheses;t.banner_pathin view
5b — Add form (public/admin/add.php)
- "Image bannière" file input added after couverture row
5c — Add action (public/admin/actions/formulaire.php)
- Banner upload: MIME check, 5 MB cap, save to
banners/, callsetBannerPath()
5d — Edit form (public/admin/edit.php)
- Banner preview img shown; remove_banner checkbox; new banner upload input
- POST handler: unlinks old file on remove; processes new upload via
setBannerPath()
5e — src/Database.php
banner_pathexposed via view — verified;getThesisById()/getPublishedTheses()pick it up automaticallysetBannerPath(int $thesisId, ?string $path): voidadded
6 — Home page: gradient placeholder cards & random ordering
6a — Gradient placeholder for cards without a banner
public/index.php— gradient placeholder using HSL hue from thesis IDpublic/assets/main.css—.card__media--gradient,.card__gradient-author,.card__gradient-titlestyles added
6b — Banner image as card thumbnail
public/index.php— checksbanner_pathfirst, falls through to gradient
6c — Random ordering from the latest year
src/Database.php—getLatestYearTheses(int $limit = 24)+getLatestPublishedYear()public/index.php— default home view uses random latest-year selection; paginated view for?year=Xand?page=N; info label shown
Refactor: M2M tags via tags + thesis_tags junction table
The current schema stores keywords in a keywords table joined via thesis_keywords.
The field column is named keyword (not name), breaking the naming convention used by
every other lookup table (orientations.name, format_types.name, etc.).
More critically, buildSearchConditions and the view v_theses_full filter keywords
through GROUP_CONCAT strings with LIKE, bypassing the junction table entirely.
Goal: rename the tables and column to the canonical M2M pattern (tags, thesis_tags,
tags.name), add the missing index, and rewrite all tag queries to use a proper JOIN.
1 — Schema migration (storage/schema.sql + live DB)
- Rename table
keywords→tags; columnkeyword→name - Rename junction
thesis_keywords→thesis_tags; FKkeyword_id→tag_id - PK on
thesis_tags(tag_id, thesis_id);idx_tags_name; updated index names - Views
v_theses_full/v_theses_publicusethesis_tags/tags.name - Migration
storage/migrations/001_rename_keywords_to_tags.sqlwritten and applied
2 — src/Database.php
findOrCreateTag()added;findOrCreateKeyword()is a backwards-compat aliasgetUsedTags()rewritten with proper M2M JOIN;getUsedKeywords()alias keptbuildSearchConditions: keyword/query useEXISTSsubquery onthesis_tags/tags- All conditions prefixed with
vp.to match view alias;vpalias added to search queries
3 — Admin write paths
public/admin/actions/formulaire.php: usesfindOrCreateTag+thesis_tagspublic/admin/edit.php:DELETE FROM thesis_tags+findOrCreateTag+thesis_tags
4 — Public read paths
public/search.php: fixed$kw['keyword']→$kw['name'](tag column rename)getUsedKeywords()alias delegates togetUsedTags()— no functional change neededpublic/tfe.php:$data['keywords']still works — view column name unchangedtemplates/search-bar.php: no keyword param refs — verified
5 — Admin tag management UI (/admin/tags.php)
5a — src/Database.php
getAllTagsWithCount(),renameTag(),mergeTag(),deleteTag()
5b — public/admin/tags.php
- Auth guard, CSRF, table with rename/merge/delete per row, inline forms
5c — public/admin/actions/tag.php
- Routes on
$_POST['action']: rename, merge, delete
5d — Nav & routing
templates/admin/head.php: "Mots-clés" nav link added
5e — Propagation safety
- mergeTag() uses INSERT OR IGNORE to avoid PK conflicts; deleteTag() cascades via FK
6 — Tests
tests/Unit/DatabaseTest.php: tests 5–7 cover findOrCreateTag, getUsedTags, aliastests/Integration/SearchTest.php: tests 4–6 cover tag-filter subquery, full-text query, count consistency
6 — Fixtures / seed data
storage/fixtures/CreateTestDatabase.php: updated totags/thesis_tags/findOrCreateTag()
Feature: Mode Maintenance
- Storage flag file
storage/maintenance.flag(created on demand) - Public gate in
config/bootstrap.php— blocks non-admin routes when flag exists public/maintenance.php(503 page, minimal dark UI)public/admin/actions/maintenance.php(POST: enable/disable)- Admin UI toggle in
public/admin/index.php(bar with status + action button)
Feature: TFE Visibility States (publique / interne / interdit)
- DB migration
002_add_visibility.sql— seeds access_types rows (already existed) src/Database.php—setVisibility(),bulkSetVisibility(),getAccessTypes()public/media.php— blocks thesis files when access_type_id = 3 (Interdit)public/tfe.php— shows access type, context_note, hides files for Interditpublic/admin/edit.php— access_type_id select + context_note textareapublic/admin/index.php— three-state access badge per rowpublic/admin/actions/visibility.php— single + bulk visibility update
Pending
- Add flake.nix for Nix-based PHP dev environment
- Add favicon (
<link rel="icon">→ admin_favicon.svg) to all pages; nginx 204 for /favicon.ico - Remove 100-item cap from répertoire student index:
getAllPublishedTheses()fetches all published theses; search results remain paginated at 30/page - Cover image fallback for home grid cards: batch-load
thesis_filescovers for theses withoutbanner_path; resolution order: banner → cover → gradient
Admin / Server
- Create
scripts/setup-server.sh(one-time server setup: group, ownership, setgid 2775 on dirs) - Add
just setup-serverrecipe (rsync + run setup-server.sh on remote) - Exclude
.claudeand.pifrom rsync deploy - Update
docs/SERVER_SETUP.mdwith correct permissions rationale and troubleshooting - Add server status view in admin panel (nginx + php-fpm health, site HTTP check)
- Add server log viewer in admin panel (tail nginx error/access logs via SSH or log endpoint)
- Add nginx config deploy flow to admin panel (upload
scripts/deploy-server.sh, run remotely) - Add admin user management UI (wraps
scripts/manage-admin-users.shon server)