mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
- 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
337 lines
14 KiB
Markdown
337 lines
14 KiB
Markdown
# TODO
|
||
|
||
## Styling Redesign (matching design images)
|
||
|
||
- [x] Redesign shared nav bar (purple gradient top, flat, POSTERG / RÉPERTOIRE / À PROPOS)
|
||
- [x] Redesign shared search bar (full-width, icon, bottom border only, white bg)
|
||
- [x] Rewrite `common.css` (nav + search bar components)
|
||
- [x] Rewrite `main.css` (home page — white bg, media card grid, label below)
|
||
- [x] Rewrite `search.css` (répertoire index — 4-col ANNÉES/CATÉGORIES/ÉTUDIANTES/MOTS-CLÉS)
|
||
- [x] Rewrite `tfe.css` (TFE page — 2-col, large author/title left, media right)
|
||
- [x] Add `apropos.css` (À Propos — 2-col, large monospace text)
|
||
- [x] Rewrite `admin.css` (dark bg, purple gradient nav, bottom-border-only form inputs)
|
||
- [x] Update `templates/nav.php` (new shared nav partial)
|
||
- [x] Update `templates/search-bar.php` (new shared search bar partial)
|
||
- [x] Rewrite `public/index.php` (home page with new layout)
|
||
- [x] Rewrite `public/search.php` (répertoire index view + search results view)
|
||
- [x] Rewrite `public/tfe.php` (individual TFE page)
|
||
- [x] Create `public/apropos.php` (À Propos page)
|
||
- [x] Rewrite `templates/admin/head.php` (admin nav)
|
||
- [x] Rewrite `templates/admin/footer.php` (clean close)
|
||
- [x] Rewrite `public/admin/add.php` (form with row layout)
|
||
- [x] Rewrite `public/admin/index.php` (dark table)
|
||
- [x] Rewrite `public/admin/edit.php` (form with row layout)
|
||
- [x] Rewrite `public/admin/login.php` (centered dark login box)
|
||
- [x] Rewrite `public/admin/thanks.php` (dark info cards)
|
||
- [x] Rewrite `public/admin/import.php` (clean dark form)
|
||
|
||
## Justfile / Ops
|
||
|
||
- [x] Simplify `serve` and `deploy` to one recipe each
|
||
- [x] Remove sysadmin recipes (server-logs, server-status, deploy-nginx, deploy-admin-tools)
|
||
- [x] Extract server scripts to `scripts/` (deploy-server.sh, manage-admin-users.sh)
|
||
- [x] Guard `deploy-db` against overwriting existing remote database
|
||
- [x] 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`.
|
||
|
||
- [x] **`public/licence.php`** — new public page; fetches content from `pages` table
|
||
(slug `'licenses'`); renders with Parsedown Markdown; uses `apropos.css` layout
|
||
- [x] **`templates/nav.php`** — add "Licence" link between "Répertoire" and "À Propos";
|
||
apply `site-nav__link--active` when `$currentNav === 'licence'`
|
||
- [x] The `pages` table 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`
|
||
|
||
- [x] `getPage(string $slug): array|null` — `SELECT * FROM pages WHERE slug = ?`
|
||
- [x] `savePage(string $slug, string $content): void` — throws if slug not found
|
||
- [x] `getAllPages(): array` — for listing in admin
|
||
|
||
#### 2b — Admin pages editor UI
|
||
|
||
- [x] **`public/admin/pages.php`** — list all editable pages; links to edit each one
|
||
- [x] **`public/admin/pages-edit.php`** — EasyMDE WYSIWYG Markdown editor via CDN
|
||
|
||
#### 2c — `public/admin/actions/page.php`
|
||
|
||
- [x] Auth guard + CSRF check + slug validation + length validation + savePage + redirect
|
||
|
||
#### 2d — Public pages render Markdown
|
||
|
||
- [x] **`public/apropos.php`** — renders `$db->getPage('about')` via Parsedown (bundled `src/Parsedown.php`)
|
||
- [x] **`public/licence.php`** — renders `$db->getPage('licenses')` via Parsedown
|
||
- [x] Parsedown bundled as `src/Parsedown.php` (zero-dependency, MIT)
|
||
|
||
#### 2e — Nav links in admin
|
||
|
||
- [x] **`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
|
||
|
||
- [x] **`storage/schema.sql`** — seed `INSERT OR IGNORE` for 8 CC licence types added
|
||
- [x] **`storage/migrations/003_seed_license_types.sql`** — migration created + applied
|
||
- [x] Verified live DB has `license_types` with 8 rows
|
||
|
||
#### 3b — `src/Database.php`
|
||
|
||
- [x] `getLicenseTypes(): array`
|
||
- [x] `getAllLicenseTypes(): array` — alias
|
||
|
||
#### 3c — Add form (`public/admin/add.php`)
|
||
|
||
- [x] Loads `$licenseTypes`; "Licence" `<select name="license_id">` added before duration
|
||
|
||
#### 3d — Add action (`public/admin/actions/formulaire.php`)
|
||
|
||
- [x] `$licenseId` parsed + included in INSERT
|
||
|
||
#### 3e — Edit form (`public/admin/edit.php`)
|
||
|
||
- [x] Loads `$licenseTypes`; raw `license_id` FK fetched directly; select pre-populated
|
||
- [x] POST handler: `license_id` included in UPDATE
|
||
|
||
#### 3f — View update
|
||
|
||
- [x] **`storage/schema.sql`** — `v_theses_full` now exposes `t.license_id` raw FK (done as part of 004/005 view rebuild)
|
||
|
||
#### 3g — TFE public page
|
||
|
||
- [x] **`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.
|
||
|
||
- [x] **`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 get `role = 'promoteur'`, `is_external = 0`
|
||
- [x] **`storage/schema.sql`** — add the two new columns to `thesis_supervisors`
|
||
definition; update `v_theses_full` to 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 existing `supervisors` column (all names) for backwards compat
|
||
- Migration SQL must DROP + CREATE the view
|
||
|
||
#### 4b — `src/Database.php`
|
||
|
||
- [x] `getThesisJury(int $thesisId): array` — fetch all supervisors for a thesis with
|
||
their role and is_external flag
|
||
- [x] `setThesisJury(int $thesisId, array $juryMembers): void` — delete + re-insert
|
||
|
||
#### 4c — Add form (`public/admin/add.php`)
|
||
|
||
- [x] Remove `promoteurice` and `promoteurice_externe` fields
|
||
- [x] 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`)
|
||
|
||
- [x] Removed old promoteurice parsing; parse jury fields; call `$db->setThesisJury()`
|
||
|
||
#### 4e — Edit form (`public/admin/edit.php`)
|
||
|
||
- [x] Load `$jury = $db->getThesisJury($thesisId)`; jury fieldset pre-populated from DB
|
||
- [x] POST handler calls `$db->setThesisJury()`
|
||
|
||
#### 4f — TFE public page (`public/tfe.php`)
|
||
|
||
- [x] 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
|
||
|
||
- [x] **`storage/migrations/005_add_banner.sql`** — `ALTER TABLE theses ADD COLUMN banner_path TEXT`
|
||
- [x] **`storage/schema.sql`** — `banner_path TEXT` in `theses`; `t.banner_path` in view
|
||
|
||
#### 5b — Add form (`public/admin/add.php`)
|
||
|
||
- [x] "Image bannière" file input added after couverture row
|
||
|
||
#### 5c — Add action (`public/admin/actions/formulaire.php`)
|
||
|
||
- [x] Banner upload: MIME check, 5 MB cap, save to `banners/`, call `setBannerPath()`
|
||
|
||
#### 5d — Edit form (`public/admin/edit.php`)
|
||
|
||
- [x] Banner preview img shown; remove_banner checkbox; new banner upload input
|
||
- [x] POST handler: unlinks old file on remove; processes new upload via `setBannerPath()`
|
||
|
||
#### 5e — `src/Database.php`
|
||
|
||
- [x] `banner_path` exposed via view — verified; `getThesisById()` / `getPublishedTheses()` pick it up automatically
|
||
- [x] `setBannerPath(int $thesisId, ?string $path): void` added
|
||
|
||
---
|
||
|
||
### 6 — Home page: gradient placeholder cards & random ordering
|
||
|
||
#### 6a — Gradient placeholder for cards without a banner
|
||
|
||
- [x] **`public/index.php`** — gradient placeholder using HSL hue from thesis ID
|
||
- [x] **`public/assets/main.css`** — `.card__media--gradient`, `.card__gradient-author`,
|
||
`.card__gradient-title` styles added
|
||
|
||
#### 6b — Banner image as card thumbnail
|
||
|
||
- [x] **`public/index.php`** — checks `banner_path` first, falls through to gradient
|
||
|
||
#### 6c — Random ordering from the latest year
|
||
|
||
- [x] **`src/Database.php`** — `getLatestYearTheses(int $limit = 24)` + `getLatestPublishedYear()`
|
||
- [x] **`public/index.php`** — default home view uses random latest-year selection;
|
||
paginated view for `?year=X` and `?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)
|
||
|
||
- [x] Rename table `keywords` → `tags`; column `keyword` → `name`
|
||
- [x] Rename junction `thesis_keywords` → `thesis_tags`; FK `keyword_id` → `tag_id`
|
||
- [x] PK on `thesis_tags(tag_id, thesis_id)`; `idx_tags_name`; updated index names
|
||
- [x] Views `v_theses_full`/`v_theses_public` use `thesis_tags`/`tags.name`
|
||
- [x] Migration `storage/migrations/001_rename_keywords_to_tags.sql` written and applied
|
||
|
||
### 2 — `src/Database.php`
|
||
|
||
- [x] `findOrCreateTag()` added; `findOrCreateKeyword()` is a backwards-compat alias
|
||
- [x] `getUsedTags()` rewritten with proper M2M JOIN; `getUsedKeywords()` alias kept
|
||
- [x] `buildSearchConditions`: keyword/query use `EXISTS` subquery on `thesis_tags`/`tags`
|
||
- [x] All conditions prefixed with `vp.` to match view alias; `vp` alias added to search queries
|
||
|
||
### 3 — Admin write paths
|
||
|
||
- [x] `public/admin/actions/formulaire.php`: uses `findOrCreateTag` + `thesis_tags`
|
||
- [x] `public/admin/edit.php`: `DELETE FROM thesis_tags` + `findOrCreateTag` + `thesis_tags`
|
||
|
||
### 4 — Public read paths
|
||
|
||
- [x] `public/search.php`: fixed `$kw['keyword']` → `$kw['name']` (tag column rename)
|
||
- [x] `getUsedKeywords()` alias delegates to `getUsedTags()` — no functional change needed
|
||
- [x] `public/tfe.php`: `$data['keywords']` still works — view column name unchanged
|
||
- [x] `templates/search-bar.php`: no keyword param refs — verified
|
||
|
||
### 5 — Admin tag management UI (`/admin/tags.php`)
|
||
|
||
#### 5a — `src/Database.php`
|
||
|
||
- [x] `getAllTagsWithCount()`, `renameTag()`, `mergeTag()`, `deleteTag()`
|
||
|
||
#### 5b — `public/admin/tags.php`
|
||
|
||
- [x] Auth guard, CSRF, table with rename/merge/delete per row, inline forms
|
||
|
||
#### 5c — `public/admin/actions/tag.php`
|
||
|
||
- [x] Routes on `$_POST['action']`: rename, merge, delete
|
||
|
||
#### 5d — Nav & routing
|
||
|
||
- [x] `templates/admin/head.php`: "Mots-clés" nav link added
|
||
|
||
#### 5e — Propagation safety
|
||
|
||
- [x] mergeTag() uses INSERT OR IGNORE to avoid PK conflicts; deleteTag() cascades via FK
|
||
|
||
### 6 — Tests
|
||
|
||
- [x] `tests/Unit/DatabaseTest.php`: tests 5–7 cover findOrCreateTag, getUsedTags, alias
|
||
- [x] `tests/Integration/SearchTest.php`: tests 4–6 cover tag-filter subquery, full-text query, count consistency
|
||
|
||
### 6 — Fixtures / seed data
|
||
|
||
- [x] `storage/fixtures/CreateTestDatabase.php`: updated to `tags`/`thesis_tags`/`findOrCreateTag()`
|
||
|
||
---
|
||
|
||
## Feature: Mode Maintenance
|
||
|
||
- [x] Storage flag file `storage/maintenance.flag` (created on demand)
|
||
- [x] Public gate in `config/bootstrap.php` — blocks non-admin routes when flag exists
|
||
- [x] `public/maintenance.php` (503 page, minimal dark UI)
|
||
- [x] `public/admin/actions/maintenance.php` (POST: enable/disable)
|
||
- [x] Admin UI toggle in `public/admin/index.php` (bar with status + action button)
|
||
|
||
---
|
||
|
||
## Feature: TFE Visibility States (publique / interne / interdit)
|
||
|
||
- [x] DB migration `002_add_visibility.sql` — seeds access_types rows (already existed)
|
||
- [x] `src/Database.php` — `setVisibility()`, `bulkSetVisibility()`, `getAccessTypes()`
|
||
- [x] `public/media.php` — blocks thesis files when access_type_id = 3 (Interdit)
|
||
- [x] `public/tfe.php` — shows access type, context_note, hides files for Interdit
|
||
- [x] `public/admin/edit.php` — access_type_id select + context_note textarea
|
||
- [x] `public/admin/index.php` — three-state access badge per row
|
||
- [x] `public/admin/actions/visibility.php` — single + bulk visibility update
|
||
|
||
---
|
||
|
||
## Pending
|
||
|
||
- [x] Add flake.nix for Nix-based PHP dev environment
|
||
- [x] Add favicon (`<link rel="icon">` → admin_favicon.svg) to all pages; nginx 204 for /favicon.ico
|
||
- [x] Remove 100-item cap from répertoire student index: `getAllPublishedTheses()` fetches all published theses; search results remain paginated at 30/page
|
||
- [ ] Thumbnail generation / cover image support for home grid cards
|
||
|
||
## Admin / Server
|
||
|
||
- [x] Create `scripts/setup-server.sh` (one-time server setup: group, ownership, setgid 2775 on dirs)
|
||
- [x] Add `just setup-server` recipe (rsync + run setup-server.sh on remote)
|
||
- [x] Exclude `.claude` and `.pi` from rsync deploy
|
||
- [x] Update `docs/SERVER_SETUP.md` with 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.sh` on server)
|