mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
337 lines
18 KiB
Markdown
337 lines
18 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
|
|
|
|
## 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 column `keywords.keyword` → `tags.name`
|
|
- [ ] Rename junction table `thesis_keywords` → `thesis_tags`; rename FK column
|
|
`thesis_keywords.keyword_id` → `thesis_tags.tag_id`
|
|
- [ ] Composite PK on `thesis_tags(tag_id, thesis_id)` (tag first — matches the lookup
|
|
pattern `WHERE 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`: replace
|
|
`LEFT JOIN keywords k ON tk.keyword_id = k.id … GROUP_CONCAT(DISTINCT k.keyword)`
|
|
with `LEFT 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()`: query `tags` table, column `name`
|
|
- [ ] `getUsedKeywords()` → `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.name
|
|
```
|
|
- [ ] `buildSearchConditions`: replace the `keywords LIKE :keyword` view-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 on `v_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` → `findOrCreateTag` and
|
|
`getUsedKeywords` → `getUsedTags` (remove after all callers updated)
|
|
|
|
### 3 — Admin write paths
|
|
|
|
- [ ] `public/admin/actions/formulaire.php`: replace `findOrCreateKeyword` +
|
|
`INSERT INTO thesis_keywords` with `findOrCreateTag` + `INSERT INTO thesis_tags`
|
|
- [ ] `public/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`; update `getUsedKeywords()` call
|
|
→ `getUsedTags()`; rename GET param `keyword` → `tag` (keep old param as alias)
|
|
- [ ] `public/tfe.php`: `$data['keywords']` → `$data['tags']` (view column rename)
|
|
- [ ] `templates/search-bar.php` (if applicable): update any hardcoded `keyword` param 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 a `thesis_count` column:
|
|
```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.name
|
|
```
|
|
- [ ] `renameTag(int $id, string $newName): void` — UPDATE `tags SET name = ? WHERE id = ?`;
|
|
unique-constraint violation must be caught and re-thrown as a user-readable
|
|
`InvalidArgumentException`
|
|
- [ ] `mergeTag(int $sourceId, int $targetId): void` — within a transaction:
|
|
1. INSERT OR IGNORE into `thesis_tags(tag_id, thesis_id)` for every row in
|
|
`thesis_tags WHERE tag_id = $sourceId` (re-point to target, skip dupes)
|
|
2. DELETE FROM `thesis_tags WHERE tag_id = $sourceId`
|
|
3. DELETE FROM `tags WHERE id = $sourceId`
|
|
- [ ] `deleteTag(int $id): void` — within a transaction:
|
|
DELETE FROM `thesis_tags WHERE tag_id = ?` first (FK cascade may handle this
|
|
after the schema migration, but an explicit delete is safer), then
|
|
DELETE FROM `tags WHERE id = ?`; reject with exception if `thesis_count > 0`
|
|
and 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 `$pageTitle` for `head.php`)
|
|
- [ ] Load `getAllTagsWithCount()` for display
|
|
- [ ] Table columns: **Mot-clé** (editable inline) · **Nb de TFE** · **Actions**
|
|
- [ ] Each row has:
|
|
- An inline `<form>` (POST to `actions/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 JS `confirm()` before
|
|
submit
|
|
- [ ] Flash success/error messages from `$_SESSION['success']` / `$_SESSION['error']`
|
|
(same pattern as `index.php`)
|
|
- [ ] Empty state: "Aucun mot-clé enregistré." if table is empty
|
|
- [ ] Stats bar (reuse `.admin-stats` CSS): 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'`: validate `tag_id` (int > 0) and `name` (non-empty, max 100 chars,
|
|
trimmed); call `renameTag()`; set `$_SESSION['success']`
|
|
- `'merge'`: validate `source_id` and `target_id` are distinct positive ints; call
|
|
`mergeTag()`; set success message including both tag names
|
|
- `'delete'`: validate `tag_id`; call `deleteTag()`; set success message
|
|
- [ ] On any exception: set `$_SESSION['error']`
|
|
- [ ] Regenerate CSRF token after every action
|
|
- [ ] Redirect back to `/admin/tags.php` in 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"; apply `active` class on `tags.php`
|
|
- [ ] `nginx/posterg.conf` (or equivalent): if using try_files / location blocks,
|
|
confirm `/admin/tags.php` and `/admin/actions/tag.php` are reachable (likely
|
|
already covered by the existing `*.php` rule, 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_full` reads from `tags` directly,
|
|
so `GROUP_CONCAT` output updates automatically — no view rebuild needed
|
|
- [ ] Renaming a tag does **not** affect `thesis_tags` rows (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
|
|
`EXISTS` subquery in `buildSearchConditions` finds them via `thesis_tags`
|
|
- [ ] Deleting a tag with `thesis_count > 0` is blocked by default (explicit
|
|
`$force` parameter required); the UI never shows a delete button for in-use
|
|
tags — double protection
|
|
- [ ] `edit.php` keyword 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.php` keyword 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 for `findOrCreateTag` round-trip
|
|
- [ ] `tests/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 use `tags` / `thesis_tags`
|
|
table names and `findOrCreateTag()`
|
|
|
|
## Feature: Mode Maintenance
|
|
|
|
Adds "Déployer" / "Maintenance" buttons in admin. When maintenance mode is on,
|
|
the public site is unavailable (503) while the admin section keeps working.
|
|
|
|
### Implementation
|
|
|
|
- [ ] **Storage** — store maintenance flag as a file on disk (`storage/maintenance.flag`)
|
|
so it requires no DB and survives restarts; flag presence = maintenance mode ON
|
|
- [ ] **Public gate** — add a check at the top of every public-facing PHP entry-point
|
|
(`public/index.php`, `public/search.php`, `public/tfe.php`, `public/media.php`,
|
|
`public/apropos.php`) via a shared helper in `config/bootstrap.php`:
|
|
`checkMaintenanceMode()` — if flag exists, render a 503 maintenance page and exit
|
|
- [ ] **Maintenance page** — create `public/maintenance.php` (503 HTML page, styled
|
|
minimally, not imported via bootstrap so it never recurses)
|
|
- [ ] **Admin action** — create `public/admin/actions/maintenance.php` (POST handler):
|
|
CSRF-validated, two actions: `enable` (touch flag file) and `disable` (unlink flag)
|
|
- [ ] **Admin UI** — add a "Site en ligne / Maintenance" status bar in
|
|
`public/admin/index.php` with "Déployer" (→ disable) and "Maintenance" (→ enable)
|
|
buttons; display current state clearly (green = en ligne, orange = maintenance)
|
|
- [ ] **Admin exemption** — admin pages (`/admin/*`) must NOT be gated; the maintenance
|
|
check must only run on public routes; admin bootstrap already bypasses it by
|
|
not calling `checkMaintenanceMode()`
|
|
- [ ] **`src/config.php`** — add `MAINTENANCE_FLAG_PATH` constant pointing to
|
|
`storage/maintenance.flag`
|
|
|
|
## Feature: TFE Visibility States (publique / interne / interdit)
|
|
|
|
Replaces the binary `is_published` with a three-state `visibility` field on each TFE,
|
|
controlling what the public site exposes. Default: `interne`.
|
|
|
|
### States
|
|
|
|
| State | DB value | Public page: metadata | Public page: files | Note displayed |
|
|
|------------|----------|-----------------------|--------------------|----------------|
|
|
| `publique` | `public` | ✓ full | ✓ via media.php | — |
|
|
| `interne` | `intern` | ✓ full | ✗ hidden | "Copies physiques disponibles aux archives de l'école." |
|
|
| `interdit` | `forbid` | ✓ full | ✗ hidden | `context_note` from DB (jury note, editable in admin) |
|
|
|
|
### Schema
|
|
|
|
- [ ] **DB migration** — `storage/migrations/002_add_visibility.sql`:
|
|
- `ALTER TABLE theses ADD COLUMN visibility TEXT NOT NULL DEFAULT 'intern'`
|
|
- `UPDATE theses SET visibility = 'public' WHERE is_published = 1` (migrate existing data)
|
|
- update `v_theses_full` view to expose `visibility` (DROP + CREATE in migration)
|
|
- update `v_theses_public` view: change filter from `is_published = 1` to
|
|
`is_published = 1` (keep; `is_published` stays for "show in listings" gate)
|
|
- **Note**: `is_published` keeps its meaning (appears in public listings at all);
|
|
`visibility` controls *what* is accessible once found; a TFE can be
|
|
`is_published = 1` + `visibility = 'intern'` → appears in list, files hidden
|
|
- [ ] **`storage/schema.sql`** — add `visibility` column + update views + add
|
|
`idx_theses_visibility` index
|
|
- [ ] **`storage/fixtures/CreateTestDatabase.php`** — seed some TFEs with each visibility
|
|
state (default: `intern`)
|
|
|
|
### Backend
|
|
|
|
- [ ] **`src/Database.php`**:
|
|
- `getThesisById($id)` — already used by `tfe.php`; ensure it returns `visibility`
|
|
and `context_note`
|
|
- `getThesis($id)` (admin) — already returns all columns; ensure `visibility` and
|
|
`context_note` pass through
|
|
- `getThesesList()` — add `visibility` to SELECT for admin list display
|
|
- `setVisibility(int $thesisId, string $state): void` — UPDATE
|
|
`theses SET visibility = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
|
|
validate `$state` ∈ `['public', 'intern', 'forbid']`
|
|
- `bulkSetVisibility(array $ids, string $state): void` — batch UPDATE for the
|
|
bulk-action bar
|
|
|
|
### Public site
|
|
|
|
- [ ] **`public/media.php`** — add visibility check:
|
|
1. Load `thesis_files` row by `path` to get `thesis_id`
|
|
2. Load `theses.visibility` for that `thesis_id` (one extra query)
|
|
3. If `visibility !== 'public'` → return 403
|
|
(This is the critical security gate; all other changes are UX)
|
|
- [ ] **`public/tfe.php`** — conditional rendering based on `$data['visibility']`:
|
|
- `publique`: render files normally (existing logic)
|
|
- `interne`: skip files block; show notice "Les copies physiques de ce travail
|
|
sont disponibles dans les archives de l'école."
|
|
- `interdit`: skip files block; show `$data['context_note']` (jury note) if set,
|
|
otherwise a generic access-restricted message
|
|
- [ ] **`public/index.php`** / **`public/search.php`** — no change needed (thumbnail
|
|
logic: if `visibility !== 'public'`, skip thumbnail even if file exists; adjust
|
|
the thumbnail-lookup block in index.php)
|
|
|
|
### Admin UI
|
|
|
|
- [ ] **`public/admin/edit.php`**:
|
|
- Replace the current `is_published` checkbox with a `<select name="visibility">`
|
|
with three labelled options (Publique / Interne / Interdit)
|
|
- Keep `is_published` checkbox separately (controls listing visibility)
|
|
- Add `<textarea name="context_note">` for jury note (shown only when
|
|
`visibility = interdit`, via JS toggle, but always submitted so admin can
|
|
pre-fill before switching state)
|
|
- Update the POST handler to save `visibility` and `context_note` via
|
|
`UPDATE theses SET visibility = ?, context_note = ? …`
|
|
- [ ] **`public/admin/index.php`**:
|
|
- Update status badge column: show three-state badge instead of binary
|
|
"Publié / En attente":
|
|
- `publique` → green "Publique"
|
|
- `interne` → blue "Interne"
|
|
- `interdit` → red "Interdit"
|
|
- Update stats bar: counts for each state instead of published/pending
|
|
- Add bulk-action buttons for visibility: "Rendre publique", "Rendre interne",
|
|
"Rendre interdit" (replaces or supplements existing publish/unpublish)
|
|
- Update JS `bulkAction()` to support new action names
|
|
- [ ] **`public/admin/actions/publish.php`** — extend to handle new visibility actions:
|
|
`set_public`, `set_intern`, `set_forbid` for single and bulk modes;
|
|
keep existing `publish`/`unpublish` actions for `is_published` toggle;
|
|
alternatively rename/create `public/admin/actions/visibility.php` as a
|
|
dedicated handler and keep `publish.php` for `is_published` only
|
|
- [ ] **`public/admin/thanks.php`** — add "Visibilité" row to the info display
|
|
|
|
## 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
|
|
- [ ] Add pagination to répertoire student index (currently capped at 100)
|
|
- [ ] 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)
|