12 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
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; rename columnkeywords.keyword→tags.name - Rename junction table
thesis_keywords→thesis_tags; rename FK columnthesis_keywords.keyword_id→thesis_tags.tag_id - Composite PK on
thesis_tags(tag_id, thesis_id)(tag first — matches the lookup patternWHERE t.name = ?) - Add index
idx_tags_name ON tags(name)(supports exact-match lookup on insert/find) - Update
idx_thesis_keywords_*index names →idx_thesis_tags_thesis,idx_thesis_tags_tag - Update view
v_theses_full/v_theses_public: replaceLEFT JOIN keywords k ON tk.keyword_id = k.id … GROUP_CONCAT(DISTINCT k.keyword)withLEFT JOIN tags t ON tt.tag_id = t.id … GROUP_CONCAT(DISTINCT t.name) - Write and test a SQLite migration script
(
storage/migrations/001_rename_keywords_to_tags.sql)
2 — src/Database.php
findOrCreateKeyword()→findOrCreateTag(): querytagstable, columnnamegetUsedKeywords()→getUsedTags(): rewrite to use proper M2M JOIN instead of querying the view:sql SELECT DISTINCT t.* FROM tags t JOIN thesis_tags tt ON t.id = tt.tag_id JOIN theses th ON tt.thesis_id = th.id WHERE th.is_published = 1 ORDER BY t.namebuildSearchConditions: replace thekeywords LIKE :keywordview-string hack with a subquery using the junction table:sql EXISTS ( SELECT 1 FROM thesis_tags tt JOIN tags t ON t.id = tt.tag_id WHERE tt.thesis_id = theses.id AND t.name LIKE :keyword ESCAPE '\' )(search still runs onv_theses_public; the subquery references the base table)validateSearchParams: rename key'keyword'→'tag'(or keep alias for backwards-compat during transition)- Add backwards-compat alias
findOrCreateKeyword→findOrCreateTagandgetUsedKeywords→getUsedTags(remove after all callers updated)
3 — Admin write paths
public/admin/actions/formulaire.php: replacefindOrCreateKeyword+INSERT INTO thesis_keywordswithfindOrCreateTag+INSERT INTO thesis_tagspublic/admin/edit.php: same replacement in keyword update block (DELETE FROM thesis_keywords→DELETE FROM thesis_tags, insert loop)
4 — Public read paths
public/search.php: rename$keywords→$tags; updategetUsedKeywords()call →getUsedTags(); rename GET paramkeyword→tag(keep old param as alias)public/tfe.php:$data['keywords']→$data['tags'](view column rename)templates/search-bar.php(if applicable): update any hardcodedkeywordparam refs
5 — Admin tag management UI (/admin/tags.php)
The goal is a dedicated page for viewing, renaming, merging, and deleting tags, with
full referential-integrity awareness (no orphan thesis_tags rows, no broken search
results).
5a — src/Database.php — new tag-management methods
getAllTagsWithCount(): array— return all tags with athesis_countcolumn:sql SELECT t.id, t.name, COUNT(tt.thesis_id) AS thesis_count FROM tags t LEFT JOIN thesis_tags tt ON t.id = tt.tag_id GROUP BY t.id ORDER BY t.namerenameTag(int $id, string $newName): void— UPDATEtags SET name = ? WHERE id = ?; unique-constraint violation must be caught and re-thrown as a user-readableInvalidArgumentExceptionmergeTag(int $sourceId, int $targetId): void— within a transaction: 1. INSERT OR IGNORE intothesis_tags(tag_id, thesis_id)for every row inthesis_tags WHERE tag_id = $sourceId(re-point to target, skip dupes) 2. DELETE FROMthesis_tags WHERE tag_id = $sourceId3. DELETE FROMtags WHERE id = $sourceIddeleteTag(int $id): void— within a transaction: DELETE FROMthesis_tags WHERE tag_id = ?first (FK cascade may handle this after the schema migration, but an explicit delete is safer), then DELETE FROMtags WHERE id = ?; reject with exception ifthesis_count > 0and the caller did not pass$force = true
5b — public/admin/tags.php — list + inline-edit view
- Auth guard:
AdminAuth::requireLogin()at top; CSRF token in session - Page title:
"Gestion des mots-clés"(reuses$pageTitleforhead.php) - Load
getAllTagsWithCount()for display - Table columns: Mot-clé (editable inline) · Nb de TFE · Actions
- Each row has:
- An inline
<form>(POST toactions/tag.php, action=rename) pre-filled with the current tag name, so the admin can edit in-place and submit per row - A Fusionner → button that reveals a
<select>of other tags and a confirm submit (POST action=merge, fields:source_id,target_id) - A Supprimer button — disabled / greyed if
thesis_count > 0; otherwise submits POST action=delete, field:tag_id; requires JSconfirm()before submit
- An inline
- Flash success/error messages from
$_SESSION['success']/$_SESSION['error'](same pattern asindex.php) - Empty state: "Aucun mot-clé enregistré." if table is empty
- Stats bar (reuse
.admin-statsCSS): total tag count, total TFE with ≥1 tag, total tags with 0 TFE (orphan count)
5c — public/admin/actions/tag.php — POST action handler
- Auth guard + CSRF check (same pattern as
publish.php) - Route on
$_POST['action']:'rename': validatetag_id(int > 0) andname(non-empty, max 100 chars, trimmed); callrenameTag(); set$_SESSION['success']'merge': validatesource_idandtarget_idare distinct positive ints; callmergeTag(); set success message including both tag names'delete': validatetag_id; calldeleteTag(); set success message
- On any exception: set
$_SESSION['error'] - Regenerate CSRF token after every action
- Redirect back to
/admin/tags.phpin all cases
5d — Nav & routing
templates/admin/head.php: add nav link<a href="/admin/tags.php" …>Mots-clés</a>between "Ajouter un TFE" and "Importer une liste de TFE"; applyactiveclass ontags.phpnginx/posterg.conf(or equivalent): if using try_files / location blocks, confirm/admin/tags.phpand/admin/actions/tag.phpare reachable (likely already covered by the existing*.phprule, but verify)
5e — Propagation safety checklist
These must remain unbroken after the tag-management UI is live:
- Renaming a tag updates
tags.name;v_theses_fullreads fromtagsdirectly, soGROUP_CONCAToutput updates automatically — no view rebuild needed - Renaming a tag does not affect
thesis_tagsrows (only the name column changes); search results remain intact - Merging tag A→B: all theses that had tag A now appear under tag B; the
EXISTSsubquery inbuildSearchConditionsfinds them viathesis_tags - Deleting a tag with
thesis_count > 0is blocked by default (explicit$forceparameter required); the UI never shows a delete button for in-use tags — double protection edit.phpkeyword field renders the current CSV of tag names from$thesis['keywords'](view column); after a rename the edit page will show the new name on next load — no extra work needed- The public
search.phpkeyword filter ($_GET['tag']) uses tag names; after a rename, any bookmarked URL with the old name will return 0 results (expected behaviour — no redirect needed, but note it in a code comment)
6 — Tests
tests/Unit/DatabaseTest.php: add test forfindOrCreateTaground-triptests/Integration/SearchTest.php: add test for tag-filter search using the new subquery path (not the LIKE-on-GROUP_CONCAT path)
6 — Fixtures / seed data
storage/fixtures/CreateTestDatabase.php: update to usetags/thesis_tagstable names andfindOrCreateTag()
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 - Add pagination to répertoire student index (currently capped at 100)
- Thumbnail generation / cover image support for home grid cards
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)