docs: verify and refactor documentation to match current codebase

This commit is contained in:
Pontoporeia
2026-08-24 11:31:38 +02:00
parent b2cdbd0174
commit e9747edce0
17 changed files with 1006 additions and 1442 deletions
+3
View File
@@ -62,3 +62,6 @@ app/public/assets/dist/
.phpunit.result.cache .phpunit.result.cache
coverage/ coverage/
# td (TODO manager) local database
.todo.sqlite
+18 -25
View File
@@ -6,36 +6,26 @@ Répertoire des travaux de fin d'études de l'[ERG](https://erg.be) (École de R
## Requirements ## Requirements
- PHP 8.4 - PHP ≥ 8.4 (with `ext-json`, `ext-openssl`, `ext-pdo`, `ext-sqlite3`)
- SQLite3 (`php8.4-sqlite3`) - Composer
- Node.js / npm
- nginx (production) - nginx (production)
## Development ## Development
### MacOS
Logiciels:
- un IDE pour éditer → VSCode
- git (ou une interface graphique) pour partager les modifications → git-gui (officiel) ou Github Desktop
- un server web avec PHP pour visualiser le project dans le navigateur → MAMP
### Workflow
0. Faire un changement dans ton IDE
1. Démarrer le site via MAMP, en sélectionnant le dossier `public`
2. Vérifier que ça marche sur le site en local, depuis ton navigateur
3. Une fois qu'un changement spécifique est fait, `commit` les changements sur les fichiers qui sont relatif à ce changement
4. Vérifier que vous avez syncroniser avec le `remote` → `pull` + `rebase` ! pas merge
5. `push` les changements vers le remote
```bash ```bash
just serve # http://localhost:8000 (public) and /admin/ # one-time
composer install
npm ci
# run the dev server (builds assets, applies migrations, opens a browser)
just dev # http://127.0.0.1:8000 (public) and /admin/
just stop # stop it
``` ```
Live CSS/JS rebuilds happen automatically via a chokidar watcher while `just dev` runs.
See [`docs/development.md`](docs/development.md) for the full workflow.
## Deployment ## Deployment
Files are pushed to the server with rsync — there is no repo on the remote. Files are pushed to the server with rsync — there is no repo on the remote.
@@ -68,8 +58,11 @@ just deploy-nginx
## Security notes ## Security notes
- Admin panel protected by PHP session (`AdminAuth`) — password-only, no username - Admin panel protected by PHP session (`AdminAuth`) — password-only, no username
- Uploads stored outside webroot, served via controlled `media.php` - Uploads stored outside webroot, served via controlled `/media` (`MediaController`)
- Rate limiting on public search (`src/RateLimit.php`) - Rate limiting on public search (`app/src/RateLimit.php`)
- See `nginx/docs/SECURITY_HEADERS.md` for security headers reference - See `nginx/docs/SECURITY_HEADERS.md` for security headers reference
## More docs
See [`docs/README.md`](docs/README.md) for the full documentation index.
+68 -124
View File
@@ -1,145 +1,89 @@
# Admin Panel Structure # Admin Panel Structure
This directory contains the admin panel for managing XAMXAM thesis database. This directory and `app/templates/admin/` contain the admin panel for managing
the XAMXAM TFE database.
## Directory Structure ## Entry points (`app/public/admin/`)
``` | File | Purpose |
public/admin/ |------|---------|
├── index.php # List all theses (main page) | `index.php` | List all theses (main page; hosts the inline CSV import + tabs for list/trash) |
├── add.php # Add new thesis form | `add.php` | Add new thesis form |
├── edit.php # Edit existing thesis form | `edit.php` | Existing thesis form |
├── import.php # CSV import form | `recapitulatif.php` | Post-submission recap |
├── recapitulatif.php # Recap page after submission | `cleanup.php` | Orphaned-draft / storage cleanup |
├── actions/ # Backend processing scripts (no HTML output) | `system.php` | System dashboard (logs, SMTP/PeerTube status) |
│ ├── formulaire.php # Process thesis submission from add.php | `contenus.php` | Editable content (pages, contacts) |
│ └── publish.php # Toggle publish/unpublish status | `contenus-edit.php` | Edit a content page |
├── inc/ # Shared templates | `acces.php` | Share-link management |
│ ├── head.php # HTML head, CSS, navigation | `file-access.php` | Restricted-file access requests |
│ └── footer.php # HTML footer | `account.php` | Admin account / password |
└── data/ # Upload directory (not in git) | `login.php` | Login (session) |
├── theses/ # PDF files | `import.php` | Redirects to `/admin/` (CSV import is inline in `index.php`) |
└── covers/ # Cover images | `status.php`, `markdown-cheatsheet-fragment.php`, `*fragment.php` | HTMX fragments / helpers |
```
## File Types ### Backend actions (`app/public/admin/actions/`)
### User-Facing Templates (Root Directory) Process forms and redirect (no HTML output):
Files that display HTML to users:
- **index.php** - Lists all theses with filters and bulk actions
- **add.php** - Form to add a new thesis
- **edit.php** - Form to edit an existing thesis
- **import.php** - CSV import interface
- **recapitulatif.php** - Success confirmation page
### Backend Scripts (actions/) - `formulaire.php` — thesis create submission (`ThesisCreateController::submit()`)
Files that process forms and redirect (no HTML output): - `edit.php` — thesis edit submission (`ThesisEditController::save()`)
- **formulaire.php** - Processes thesis submission from add.php - `export-csv.php`, `export-db.php`, `export-files.php` — see `docs/export.md`
- **publish.php** - Handles publish/unpublish actions - `filepond/` — FilePond async upload endpoints
- many others: `publish`, `delete`, `corbeille` (trash), `draft`, `visibility`,
### Shared Templates (inc/) `tag`, `language`, `form-help*`, `page`, `apropos`, `smtp-test`,
Reusable HTML components: `peertube-*`, `maintenance`, `settings`, `account`, `access-request`,
- **head.php** - HTML head, CSS links, navigation menu `acces-etudiante`, `cleanup-*`
- **footer.php** - HTML footer
## Workflow
### Adding a Thesis
1. User visits `add.php` (displays form)
2. User submits form to `actions/formulaire.php` (processes data)
3. On success, redirects to `recapitulatif.php?id=123`
4. On error, redirects back to `add.php` with error message
### Publishing/Unpublishing
1. User clicks publish/unpublish button in `index.php`
2. Form submits to `actions/publish.php` (processes action)
3. Redirects back to `index.php` with success/error message
## Security
- All pages require HTTP Basic Auth (configured in nginx) — primary layer
- All pages require PHP session auth (`AdminAuth::requireLogin()`) — defence-in-depth
- CSRF tokens protect all forms
- File uploads validated and sanitized
- Database queries use prepared statements
- Upload directory outside public/ in production
See `nginx/PHP_AUTH_LAYER.md` for details on the dual-auth architecture.
## Templates ## Templates
The `inc/` folder contains shared templates: View templates live under `app/templates/admin/` (not in `public/`):
- `head.php` - Included at the top of each page (DOCTYPE, CSS, nav) - `app/templates/admin/*.php` — page layouts
- `footer.php` - Included at the bottom of each page (closing tags) - `app/templates/admin/partials/` — shared fragments (toasts, dialogs, toc, …)
The public/partage and form partials live in `app/templates/partials/` and
`app/templates/partage/`.
## Auth
- **PHP session auth** (`src/AdminAuth.php`) via `AdminAuth::requireLogin()` is
the only authentication layer. The old nginx `auth_basic` layer has been
removed — see `docs/security.md` and `nginx/docs/PHP_AUTH_LAYER.md`.
- All forms include a CSRF token from `$_SESSION['csrf_token']`.
- Inputs use PDO prepared statements; uploads validated and stored outside the
webroot (`app/storage/`).
## Bootstrap / routing
Entry pages bootstrap the app and set up the environment:
Usage:
```php ```php
<?php include "inc/head.php" ?> require_once __DIR__ . '/../../bootstrap.php'; // defines APP_ROOT, autoload, config
<!-- Page content here --> require_once APP_ROOT . '/src/AdminAuth.php';
<?php include "inc/footer.php" ?>
```
## URL Structure
- `/admin/` - List theses (index.php)
- `/admin/add.php` - Add new thesis
- `/admin/edit.php?id=123` - Edit thesis #123
- `/admin/import.php` - Import CSV
- `/admin/recapitulatif.php?id=123` - Recap page
Backend actions (not directly accessed):
- `/admin/actions/formulaire.php` - Form processor
- `/admin/actions/publish.php` - Publish toggle
## Development
### Adding a New Page
1. Create the template in `/admin/yourpage.php`:
```php
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../lib/AdminAuth.php';
AdminAuth::requireLogin(); AdminAuth::requireLogin();
$pageTitle = "Your Page Title";
?>
<?php include "inc/head.php" ?>
<!-- Your content here -->
<?php include "inc/footer.php" ?>
``` ```
2. Add navigation link in `inc/head.php` if needed `APP_ROOT` is the `app/` directory. Database access is via
`app/src/Database.php`; form logic lives in `app/src/Controllers/` and
`app/src/Form/`.
### Adding a New Action ## URL structure
1. Create the script in `/admin/actions/youraction.php`: - `/admin/` — list theses (index.php)
```php - `/admin/add.php` — add thesis
<?php - `/admin/edit.php?id=N` — edit thesis
require_once __DIR__ . "/../../config/bootstrap.php"; - `/admin/cleanup.php`, `/admin/system.php`, `/admin/acces.php`, etc.
require_once __DIR__ . '/../../lib/AdminAuth.php'; - `/admin/actions/…` — backend processors
AdminAuth::requireLogin();
// Verify CSRF token ## Development guide
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
$_SESSION['error'] = "Security error";
header('Location: ../index.php');
exit;
}
// Process action... **Add a page:** create `app/templates/admin/yourpage.php`, add a thin
`app/public/admin/yourpage.php` entry that bootstraps + requires the template,
and add navigation in `app/templates/admin/partials/admin-toc.php`.
// Redirect **Add an action:** create `app/public/admin/actions/youraction.php` that
header('Location: ../yourpage.php'); bootstraps, requires login, verifies the CSRF token, performs the work, and
exit; redirects back to the referring admin page.
```
2. Create form in template that posts to `actions/youraction.php` See `docs/development.md` for the general workflow (dev server, builds,
tests, linting).
## Notes
- Bootstrap path from actions/: `__DIR__ . "/../../config/bootstrap.php"`
- Redirects from actions/: use `../` prefix (e.g., `../index.php`)
- Database class: `require_once __DIR__ . '/../../lib/Database.php'`
- All forms must include CSRF token from `$_SESSION['csrf_token']`
+25 -196
View File
@@ -1,206 +1,35 @@
# Database Documentation # Database documentation
Complete documentation for the XAMXAM thesis database. XAMXAM stores its data in a SQLite database. See also the top-level
[`docs/database.md`](../../docs/database.md).
## 📚 Available Documentation ## Quick start
### 1. **[DATABASE_SPECIFICATION.md](DATABASE_SPECIFICATION.md)** ⭐ - **Database file:** `xamxam.db`
**Complete technical specification** - 25KB comprehensive document - **Schema (baseline + seed):** `schema.sql`
- **Migrations:** applied set under `app/migrations/applied/` (run via `just migrate`)
**Contents:**
- Complete table definitions with all columns
- Entity relationship diagrams
- Junction table specifications
- Lookup table values
- Business rules and workflows
- Sample queries and use cases
- Instructions for requesting schema changes
**Use when:** You need complete technical details about the database structure.
---
### 2. **[QUICK_SCHEMA_REFERENCE.md](QUICK_SCHEMA_REFERENCE.md)** 🚀
**Quick reference guide** - 5KB at-a-glance reference
**Contents:**
- Table summary
- Key relationships diagram
- Core fields reference
- Predefined lookup values
- Common SQL queries
- Constraint summary
**Use when:** You need quick lookup or common query examples.
---
### 3. **[schema.sql](schema.sql)** 💾
**The actual SQL schema** - Executable SQL file
**Contents:**
- Complete CREATE TABLE statements
- Indexes and triggers
- Predefined data (orientations, AP programs, etc.)
- Views for common queries
**Use when:** Setting up or resetting the database.
---
## 🚀 Quick Start
### View Database Schema
```bash ```bash
# Read the quick reference just query # open an interactive sqlite3 shell on xamxam.db
cat database/QUICK_SCHEMA_REFERENCE.md just init-db # (re)create xamxam.db from schema.sql
just reset-db # rm xamxam.db + init-db
# Or full specification just migrate # run pending migrations
cat database/DATABASE_SPECIFICATION.md
``` ```
### Initialize Database ## Files here
```bash
# Create test database from schema
just init-test-db
# Create with sample data | Path | Purpose |
just create-fixtures |------|---------|
``` | `xamxam.db` | The live SQLite database |
| `schema.sql` | Full, fully-migrated schema + seed data (regenerated from the local DB) |
| `backups/` | `just backup-snapshot` hot-backups (`*.db.gz`) |
| `cache/` | Runtime cache (rate limits, etc.) |
| `logs/` | Runtime logs (admin, audit) — **outside the webroot** |
| `covers/` | Cover images |
| `theses/`, `tfe/`, `tmp/` | Uploaded files / staging |
### Query Database ## See also
```bash
# Open SQLite prompt
just query-db
# Show specific thesis - [`docs/database.md`](../../docs/database.md) — schema reference, tables, common SQL
just show-thesis 42 - [`docs/import.md`](../../docs/import.md) — CSV import format
``` - [`docs/export.md`](../../docs/export.md) — CSV / DB / files export + restore
## 📝 Making Schema Changes
### Step 1: Document Your Request
Format:
```
**Table:** [table_name]
**Change Type:** [add/modify/remove]
**What:** [description]
**Why:** [reason/use case]
**Example Data:** [samples]
```
### Step 2: Specify Details
For **new columns**:
- Column name
- Data type (TEXT, INTEGER, BOOLEAN, DATETIME)
- NULL/NOT NULL
- Default value
- Indexes needed?
For **new tables**:
- Table name
- All columns
- Relationships to existing tables
- Sample data
### Step 3: Provide Context
Include:
- Use case scenario
- Who will use it?
- How will it be displayed?
- Any constraints?
### Example Request
```
**Table:** theses
**Change Type:** add column
**What:** Add column to track if thesis won an award
**Why:** Need to highlight award-winning theses on homepage
**Column Name:** has_award
**Data Type:** BOOLEAN
**Default:** 0 (false)
**Example:** 1 for "Prix du Jury 2025" winner
```
## 🗂️ Database Structure Overview
```
┌─────────────┐
│ theses │ ◄── Main table (500+ records/year)
└──────┬──────┘
│
├──► authors (via thesis_authors)
├──► supervisors (via thesis_supervisors)
├──► keywords (via thesis_keywords)
├──► languages (via thesis_languages)
├──► formats (via thesis_formats)
├──► thesis_files (attachments)
│
└──► Lookup tables:
• orientations
• ap_programs
• finality_types
• access_types
• license_types
```
## 📊 Key Statistics
- **Core tables:** 3 (theses, authors, supervisors)
- **Junction tables:** 5 (many-to-many relationships)
- **Lookup tables:** 7 (predefined values)
- **Support tables:** 2 (files, pages)
- **Views:** 2 (full data, public only)
- **Indexes:** 11 (for performance)
- **Triggers:** 4 (auto-update timestamps)
## 🔍 Common Scenarios
### Scenario 1: Student Submits Thesis
1. Create record in `theses` (is_published=0)
2. Add author to `authors`, link via `thesis_authors`
3. Add supervisor(s) to `supervisors`, link via `thesis_supervisors`
4. Set `orientation_id`, `ap_program_id`, `finality_id`
5. Upload file to `thesis_files`
6. Add keywords via `thesis_keywords`
7. Set `submitted_at` timestamp
### Scenario 2: Admin Publishes Thesis
1. Verify all required fields present
2. Set `defense_date`
3. Set `jury_points`
4. Optional: add `context_note`
5. Set `is_published = 1`
6. Set `published_at = CURRENT_TIMESTAMP`
### Scenario 3: Public User Searches
Query `v_theses_public` view with filters:
- By year
- By orientation
- By keyword
- By author name
- Full-text search in title/synopsis
## 🛠️ Development Workflow
### Local Development
1. Use `xamxam.db` for development
2. Create via `just init-db`
3. Test queries before deployment
## 📞 Need Help?
1. **Quick lookup** → Read `QUICK_SCHEMA_REFERENCE.md`
2. **Complete details** → Read `DATABASE_SPECIFICATION.md`
3. **Schema changes** → Follow format in this README
4. **SQL examples** → Check `QUICK_SCHEMA_REFERENCE.md`
## 🔗 Related Documentation
- [Deployment Guide](../nginx/DEPLOYMENT_COMPLETE.md)
- [Repository Structure](../REPOSITORY_STRUCTURE_ANALYSIS.md)
- [Test Database Guide](../nginx/TEST_DATABASE_SETUP.md)
+26 -14
View File
@@ -1,21 +1,33 @@
# CSS Architecture # CSS Architecture
## File Structure ## Source files (`app/public/assets/css/`)
- **variables.css** — all CSS custom properties (single source of truth for every color/token) - **`variables.css`** — all CSS custom properties (colors, spacing, sizing tokens)
- **common.css** — reset, header/nav, search bar, accessibility utilities (loaded on all pages) - **`colors.css`** — colour tokens (referenced by `variables.css`)
- **main.css** — home page - **`reset.css`** / **`modern-normalize.min.css`** — resets
- **search.css** — search/directory page - **`base.css`** — base element styles
- **tfe.css** — individual thesis page - **`common.css`** — shared components (header/nav, search bar, accessibility, buttons)
- **apropos.css** — about + licence pages - **`typography.css`** — type scale
- **system.css** — admin system dashboard - **`utilities.css`** — utility classes and spacing tokens
- **admin.css** — admin section (loaded alongside `common.css` on every admin page) - **`style.css`** — main public entry point (via `@import` chain)
- **modern-normalize.min.css** — third-party reset (minified, do not edit) - Page-specific: **`public.css`** (home), **`tfe.css`**, **`repertoire.css`**, **`content-page.css`** (about/licences), **`system.css`** (admin system dashboard), **`file-access.css`**, **`admin.css`** (admin section)
- Form: **`form-base.css`** + **`form-admin.css`** (plus FilePond vendor CSS)
## Build / bundling
Source CSS is **bundled and minified** by `scripts/build-css.mjs` (lightningcss)
into `app/public/assets/dist/*.min.css`:
- `base.min.css` — resolves the `@import` chain in `style.css` into one file (eliminates ~17 sequential imports)
- `admin.min.css` — minified `admin.css`
- `form.min.css` — `form-base.css` + `form-admin.css` + FilePond vendor CSS
- plus individual `.min.css` for the page-specific files
Run with `just build-css` / `just dev-build`. Never commit hand-edits into
`dist/` — they are generated.
## Rules ## Rules
- Every color value lives in `variables.css` as a CSS custom property. - Colour values live in `variables.css`/`colors.css` as custom properties; avoid hardcoding hex/rgb in other files.
- No hardcoded hex, rgb(), or rgba() in any other file.
- All files `@import url("./variables.css")` at the top.
- Admin and public share the same token names — no separate admin theme. - Admin and public share the same token names — no separate admin theme.
- No dark-mode media query. System page uses the same light tokens as the rest of the admin section. - `modern-normalize.min.css` and FilePond vendor CSS are third-party (minified — do not edit).
+77
View File
@@ -0,0 +1,77 @@
# Documentation index
This directory mixes **current reference** docs, **proposals/plans**, and
**historical/archived** analysis. Use this index to find the right document.
> **Note on naming:** XAMXAM was previously *Post-ERG* (and the code was once
> organised under `posterg-website/`, `apps/`, `front-backend/`,
> `/var/www/posterg/`). References to `posterg`, `apps/public/inc/header.php`,
> `posterg.db`, `just serve`, `php-live-reload`, `tests/run-tests.php`, etc. in
> historical docs reflect the old layout. The live paths are now `app/`,
> `app/public/`, `app/storage/xamxam.db`, `just dev`, PHPUnit, and
> `/var/www/xamxam/` on the server.
## Current reference (kept up to date)
| Doc | Contents |
|-----|----------|
| [development.md](development.md) | Dev workflow, structure, builds, testing, linting |
| [deployment.md](deployment.md) | Server setup, deploy, backups, rollback |
| [database.md](database.md) | SQLite schema, migrations, tables, common ops |
| [search.md](search.md) | `/search` and `/repertoire` behaviour |
| [export.md](export.md) | CSV / DB / files export + full restore procedure |
| [import.md](import.md) | CSV import format + behaviour |
| [security.md](security.md) | Current security posture |
| [file-uploads.md](file-uploads.md) | Upload surfaces, types, storage layout |
| [CSS.md](CSS.md) | CSS architecture + build |
| [bookmarklet.md](bookmarklet.md) | Form auto-fill test helper |
## Proposals / plans (proposed, not all implemented)
| Doc | Status |
|-----|--------|
| [LDAP_AUTH_PLAN.md](LDAP_AUTH_PLAN.md) + [LDAP_SPEC.md](LDAP_SPEC.md) | LDAP login — **not implemented** |
| [monolog-plan.md](monolog-plan.md) | Single Monolog logger replacing AppLogger/AdminLogger/ErrorHandler/Audit — **plan** |
| [de-librairisation.md](de-librairisation.md) | Replace bespoke SMTP/Markdown/HTTP/crypto with libraries (partly done) |
| [refactoring.md](refactoring.md) | Older refactoring proposal |
| [ANALYSIS_STRUCTURE_REORG.md](ANALYSIS_STRUCTURE_REORG.md) | Proposed structure reorg |
| [ANALYSIS_INLINE_JS_CSS_MINIFY.md](ANALYSIS_INLINE_JS_CSS_MINIFY.md) | Inline JS/CSS/minify analysis (largely actioned by the build system) |
| [backup-plan.md](backup-plan.md) | Backup plan — largely implemented (see deployment.md) |
| [repertoire-mobile-propositions.md](repertoire-mobile-propositions.md) | Mobile repertoire UI proposals |
| [cms-migration-plan.html](cms-migration-plan.html) | CMS migration plan |
| [spec-sheet.md](spec-sheet.md) | Original requirements fiche technique |
| `Proposition procédure licences_V2.pdf` | Licence procedure proposal (filename contains non-ASCII/combining chars) |
## Historical / archived (kept for context — may reference the old codebase)
| Doc | Contents |
|-----|----------|
| [LIVRAISONS_PAR_MOIS.md](LIVRAISONS_PAR_MOIS.md) | Monthly delivery log grouped by functional family |
| [migration-history.md](migration-history.md) | History of major structural migrations |
| [CURRENT_ISSUES.md](CURRENT_ISSUES.md) | Issue log (2026-05-10) — many since resolved |
| [IMMEDIATE_FIX.md](IMMEDIATE_FIX.md) | One-off fix note |
| [EVIDENCE_SUMMARY.md](EVIDENCE_SUMMARY.md) + [VM_Crash_*.md](VM_Crash_Analysis_FINAL.md) | VM crash investigation (concluded: not the app) |
| [css.md](css.md) | Old Bulma-removal writeup (pre-dates current build system; see CSS.md) |
| [php-vs-flask.md](php-vs-flask.md) | Language choice decision |
| [orm-assessment.md](orm-assessment.md) | ORM evaluation |
| [system-setup.md](system-setup.md) | Old setup notes |
| [SETUP.md](SETUP.md), [SPECS.md](SPECS.md), [TODO.md](TODO.md) | Earlier setup/spec/todo snapshots |
| [SMTP_550_POSTFIX_FIX.md](SMTP_550_POSTFIX_FIX.md) | SMTP troubleshooting record |
| [testing.md](testing.md) | PHP testing best-practices writeup |
| [test-plan.md](test-plan.md) | Manual test plan |
| [autosave-system.md](autosave-system.md), [filepond-crash-analysis.md](filepond-crash-analysis.md), [filepond-race-investigation.md](filepond-race-investigation.md) | Feature/issue deep-dives (status noted in each file) |
| [pi-session-2026-05-10T*.html](pi-session-2026-05-10T18-42-37-234Z_019e1332-ce31-70fa-87a1-aa3495b526a9.html) | Captured session log |
| [ANALYSIS_INLINE_JS_CSS_MINIFY.md](ANALYSIS_INLINE_JS_CSS_MINIFY.md) | *(see proposals)* |
| [bookmarklet.md](bookmarklet.md) | *(kept current — see above)* |
## Related documentation elsewhere
- `nginx/docs/` — nginx config, security headers, deployment, HTACCESS→nginx
- `app/storage/README.md` — DB quick-reference (schema link)
- `app/public/admin/README.md` — admin panel structure
---
**Maintenance guidance:** when updating code, update the matching *current
reference* doc in the table above. Leave *historical* docs untouched (they are
read-only context). Move newly-written analysis into the appropriate section.
+78 -82
View File
@@ -1,111 +1,107 @@
# Bookmarklet # Bookmarklet — auto-fill test form
## Auto-fill TFE form (testing) A drag-to-bookmarks helper that pre-fills an XAMXAM thesis form with dummy data
so a submit can be tested quickly.
Drag the link below to your bookmarks bar, open `/admin/add.php?mode=student`, then click it. Every field gets filled with dummy data so you can hit submit immediately. > The old `?mode=student` flag and the standalone student form no longer exist.
> Student submissions now use **share links** at `/partage/<slug>` (see the
> `share_links` table). The same form template (`app/templates/partials/form/`)
> drives admin add/edit and the partage form, so the field names below mostly
> match. The bookmarklet below targets the **admin** add form
> (`/admin/add.php`).
### Bookmarklet ## Bookmarklet
**[Remplir TFE](javascript:(function()%7Bvar%20f%3Ddocument.querySelector('form.admin-form')%3Bif(!f)return%3Bvar%20set%3Dfunction(n%2Cv)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'%22]')%3Bif(e)%7Be.value%3Dv%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bvar%20check%3Dfunction(n)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'%22]')%3Bif(e)%7Be.checked%3Dtrue%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bvar%20checkCB%3Dfunction(n%2Cv)%7Bvar%20e%3Df.querySelector('[name%3D%22'%2Bn%2B'[]%22][value%3D%22'%2Bv%2B'%22]')%3Bif(e)%7Be.checked%3Dtrue%3Be.dispatchEvent(new%20Event('change'%2C%7Bbubbles%3Atrue%7D))%3B%7D%7D%3Bset('titre'%2C'TFE%20Test%20%E2%80%94%20Impact%20des%20r%C3%A9seaux%20sociaux%20sur%20la%20pratique%20artistique%20contemporaine')%3Bset('subtitle'%2C'%C3%89tude%20de%20cas%20aupr%C3%A8s%20des%20%C3%A9tudiant%C2%B7es%20de%20l%27ERG')%3Bset('auteurice'%2C'Marie-France%20Dupont')%3Bset('mail'%2C'mfd%40testmail.be')%3Bset('synopsis'%2C'Ce%20travail%20explore%20la%20mani%C3%A8re%20dont%20les%20plateformes%20num%C3%A9riques%20ont%20transform%C3%A9%20les%20processus%20de%20cr%C3%A9ation%20artistique.')%3Bcheck('contact_public')%3Bset('jury_president'%2C'Prof.%20Jean-Luc%20Moreau')%3Bset('jury_promoteur'%2C'Dr.%20Aline%20Vandenberghe')%3Bcheck('jury_promoteur_ext')%3Bset('ann%C3%A9e'%2C'2025')%3Bset('orientation'%2C'4')%3Bset('ap'%2C'3')%3Bset('finality'%2C'1')%3BcheckCB('languages'%2C'1')%3BcheckCB('languages'%2C'2')%3BcheckCB('formats'%2C'1')%3BcheckCB('formats'%2C'3')%3Bset('tag'%2C'r%C3%A9seaux%20sociaux%2Cart%20num%C3%A9rique%2Csociologie%2Cpratique%20artistique')%3Bset('license_id'%2C'7')%3Bset('duration_info'%2C'96%20pages')%3Bset('lien'%2C'https%3A%2F%2Fexample.com%2Ftfe-test')%3Bset('access_type_id'%2C'2')%3Bcheck('cc4r')%3B%7D)())** Open `/admin/add.php` (logged in), then click the bookmarklet:
### Readable source ```
javascript:(function(){var set=function(n,v){var e=document.querySelector('[name="'+n+'"]');if(e){e.value=v;e.dispatchEvent(new Event('change',{bubbles:true}));}};var setFirst=function(n,v){var e=document.querySelector('[name="'+n+'"][type="text"]');if(e){e.value=v;e.dispatchEvent(new Event('change',{bubbles:true}));}};var checkCB=function(n,v){var e=document.querySelector('[name="'+n+'[]"][value="'+v+'"]');if(e){e.checked=true;e.dispatchEvent(new Event('change',{bubbles:true}));}};var check=function(n){var e=document.querySelector('[name="'+n+'"]');if(e){e.checked=true;e.dispatchEvent(new Event('change',{bubbles:true}));}};set('titre','TFE Test — Impact des réseaux sociaux');set('subtitle','Étude de cas auprès des étudiant·es de l\'ERG');set('auteurice','Marie-France Dupont');set('contact_visible','public');setFirst('jury_promoteur[]','Dr. Aline Vandenberghe');setFirst('jury_lecteur_interne[]','Prof. Jean-Luc Moreau');setFirst('jury_lecteur_externe[]','Prof. Kim Sølv');setFirst('jury_promoteur_ulb_name[]','Prof. Université');set('synopsis','Ce travail explore l\'impact des plateformes numériques sur la pratique artistique contemporaine.');set('orientation','4');set('ap','3');set('finality','1');checkCB('formats','1');checkCB('formats','3');set('license_id','8');set('duration_pages','96');check('cc2r');set('website_url','https://example.com/tfe-test');set('access_type_id','2');set('contact_interne','mfd@testmail.be');set('jury_points','16.5');})()
```
## Readable source
```js ```js
(function () { (function () {
var f = document.querySelector('form.admin-form'); var set = function (n, v) {
if (!f) return; var e = document.querySelector('[name="' + n + '"]');
if (e) { e.value = v; e.dispatchEvent(new Event('change', { bubbles: true })); }
var set = function (name, val) {
var e = f.querySelector('[name="' + name + '"]');
if (e) { e.value = val; e.dispatchEvent(new Event('change', { bubbles: true })); }
}; };
var check = function (name) { var setFirst = function (n, v) { // first hidden/text field of a jury array
var e = f.querySelector('[name="' + name + '"]'); var e = document.querySelector('[name="' + n + '"][type="text"]');
if (e) { e.value = v; e.dispatchEvent(new Event('change', { bubbles: true })); }
};
var checkCB = function (n, v) { // checkbox-list (formats[]) value
var e = document.querySelector('[name="' + n + '[]"][value="' + v + '"]');
if (e) { e.checked = true; e.dispatchEvent(new Event('change', { bubbles: true })); } if (e) { e.checked = true; e.dispatchEvent(new Event('change', { bubbles: true })); }
}; };
var checkCB = function (name, val) { var check = function (n) {
var e = f.querySelector('[name="' + name + '[]"][value="' + val + '"]'); var e = document.querySelector('[name="' + n + '"]');
if (e) { e.checked = true; e.dispatchEvent(new Event('change', { bubbles: true })); } if (e) { e.checked = true; e.dispatchEvent(new Event('change', { bubbles: true })); }
}; };
// ── Informations du TFE ── // ── Informations du TFE ──
set('titre', 'TFE Test — Impact des réseaux sociaux sur la pratique artistique contemporaine'); set('titre', 'TFE Test — Impact des réseaux sociaux');
set('subtitle', 'Étude de cas auprès des étudiant·es de l\'ERG'); set('subtitle', 'Étude de cas auprès des étudiant·es de l\'ERG');
set('auteurice', 'Marie-France Dupont'); set('auteurice', 'Marie-France Dupont'); // comma-separated
set('mail', 'mfd@testmail.be'); set('contact_visible', 'public'); // admin mode; partage uses 'mail'
set('synopsis', 'Ce travail explore la manière dont les plateformes numériques ont transformé les processus de création artistique.');
check('contact_public');
// ── Jury ── // ── Jury (array fields, one row each) ──
set('jury_president', 'Prof. Jean-Luc Moreau'); setFirst('jury_promoteur[]', 'Dr. Aline Vandenberghe');
set('jury_promoteur', 'Dr. Aline Vandenberghe'); setFirst('jury_lecteur_interne[]', 'Prof. Jean-Luc Moreau');
check('jury_promoteur_ext'); setFirst('jury_lecteur_externe[]', 'Prof. Kim Sølv');
setFirst('jury_promoteur_ulb_name[]', 'Prof. Université');
// ── Cadre académique ── // ── Cadre académique ──
set('année', '2025'); set('synopsis', 'Ce travail explore l\'impact des plateformes numériques sur la pratique artistique contemporaine.');
set('orientation', '4'); // Installation-Performance set('orientation', '4'); // Installation-Performance
set('ap', '3'); // Atelier Pratiques Situées set('ap', '3'); // Atelier Pratiques Situées
set('finality', '1'); // Approfondi set('finality', '1'); // Approfondie
checkCB('languages', '1'); // Français
checkCB('languages', '2'); // Anglais
checkCB('formats', '1'); // Site web checkCB('formats', '1'); // Site web
checkCB('formats', '3'); // Vidéo checkCB('formats', '3'); // Vidéo
set('tag', 'réseaux sociaux, art numérique, sociologie, pratique artistique');
// ── Métadonnées complémentaires ── // ── Métadonnées ──
set('license_id', '7'); // Tous droits réservés set('license_id', '8'); // Tous droits réservés
set('duration_info', '96 pages'); set('duration_pages', '96');
set('lien', 'https://example.com/tfe-test'); check('cc2r'); // CC2r licence
set('website_url', 'https://example.com/tfe-test');
set('access_type_id', '2'); // Interne set('access_type_id', '2'); // Interne
set('contact_interne', 'mfd@testmail.be');
// ── Licences (student mode only) ── set('jury_points', '16.5');
check('cc4r');
})(); })();
``` ```
### Lookup table reference (schema.sql seed IDs) ## Notes & current field names (verified against the form partials)
| Table | ID | Value | | Field | Notes |
|---|---|---| |-------|-------|
| **orientations** | 1 | Arts Numériques | | `titre`, `subtitle`, `auteurice` | `auteurice` is comma-separated |
| | 2 | Dessin | | `contact_visible` | admin add/edit; the partage form uses `mail` |
| | 3 | Cinéma d'animation | | `jury_promoteur[]`, `jury_lecteur_interne[]`, `jury_lecteur_externe[]`, `jury_promoteur_ulb_name[]` | one `input[type=text]` per row (`setFirst` targets the first) |
| | 4 | Installation-Performance | | `orientation`, `ap`, `finality` | `<select>`; values are DB ids |
| | 5 | Peinture | | `formats[]` | checkbox-list |
| | 6 | Photographie | | `languages[]`, `tags[]` | **pill-search** (type-ahead with hidden `name=…[]` pills) — not pre-filled by this simple helper; add pills through the search UI or dispatch the pill-create event |
| | 7 | Sculpture | | `license_id` | `<select>` of `license_types` |
| | 8 | Vidéographie | | `cc2r` | checkbox (renamed from `cc4r`) |
| | 9 | Graphisme | | `want_license` | hidden `0`; set to `1` to show the licence explanation block |
| | 10 | Typographie | | `duration_pages` | combined duration: pages int; time uses `duration_h` + `duration_m` (values in minutes) |
| | 11 | Design Numérique | | `website_url`, `website_label` | the "lien" (site web) fields |
| | 12 | Illustration | | `access_type_id` | radio: `""`, `1`=Libre, `2`=Interne, `3`=Interdit |
| | 13 | Bande-Dessinée | | `contact_interne`, `exemplaire_baiu`, `exemplaire_erg`, `is_published`, `context_note`, `jury_points`, `remarks` | other form fields |
| | 14 | Sérigraphie |
| | 15 | Gravure | ### Removed / renamed
| **ap_programs** | 1 | Narration Spéculative |
| | 2 | Design et Politique du Multiple | - `cc4r` → `cc2r`
| | 3 | Atelier Pratiques Situées | - `duration_info` → split into `duration_pages` + `duration_h`/`duration_m`
| | 4 | LIENS | - `jury_president`, single `jury_promoteur` → jury arrays above (`jury_promoteur[]` etc.)
| **finality_types** | 1 | Approfondi | - `lien` → `website_url`; `mail` in admin mode → `contact_visible`
| | 2 | Enseignement | - `contact_public` checkbox → handled via `authors.show_contact` / `contact_visible`
| | 3 | Spécialisé | - `?mode=student` → student flows go through `/partage/<slug>`
| **languages** | 1 | Français |
| | 2 | Anglais | ### Lookup ids
| **format_types** | 1 | Site web |
| | 2 | Audio | `orientations` 4=`Installation-Performance`; `ap_programs` 3=`Atelier Pratiques
| | 3 | Vidéo | Situées`; `finality_types` 1=`Approfondie`; `format_types` 1=`Site web`,
| | 4 | Performance | 3=`Vidéo`; `license_types` 8=`Tous droits réservés`; `access_types` 2=`Interne`.
| | 5 | Objet éditorial |
| | 6 | Installation | > IDs may shift after DB migrations that re-number reference rows — verify
| | 7 | Autre | > against the current `app/storage/schema.sql` seed data if a lookup fails.
| **license_types** | 1 | CC BY 4.0 |
| | 2 | CC BY-SA 4.0 |
| | 3 | CC BY-ND 4.0 |
| | 4 | CC BY-NC 4.0 |
| | 5 | CC BY-NC-SA 4.0 |
| | 6 | CC BY-NC-ND 4.0 |
| | 7 | Tous droits réservés |
| | 8 | Domaine public |
| **access_types** | 1 | Libre |
| | 2 | Interne |
| | 3 | Interdit |
+174 -244
View File
@@ -1,323 +1,253 @@
# Database Reference # Database Reference
Post-ERG SQLite database — schema, configuration, and operations. XAMXAM SQLite database — schema, configuration, and operations.
**Version:** 1.0 · **Engine:** SQLite 3 · **Mode:** WAL The database lives at **`app/storage/xamxam.db`**. The canonical schema is
**`app/storage/schema.sql`** (base tables + seed data); incremental changes are
applied through versioned **migrations** in `app/migrations/` (see the
*Schema changes* section).
> Legacy docs referenced `posterg.db`, `database/schema.sql`, and
> `config/bootstrap.php`. Those are obsolete — the schema now lives under
> `app/storage/` and migrations are handled by `app/migrations/run.php`.
--- ---
## Quick Start ## Engine & mode
- **Engine:** SQLite 3
- **Mode:** WAL (`journal_mode=wal`) for safe concurrent reads + hot backups
- **Driver:** PDO (`ext-pdo` + `ext-sqlite3`), wrapped by `app/src/Database.php`
---
## Development quick start
```bash ```bash
cd database/ just migrate # run pending migrations (creates DB from schema if missing)
sqlite3 posterg.db < schema.sql # Create DB just init-db # create DB from app/storage/schema.sql
sqlite3 posterg.db "SELECT name FROM sqlite_master WHERE type='table';" just reset-db # rm xamxam.db + init-db
sqlite3 posterg.db "SELECT * FROM orientations;" # Verify seed data just query # open an interactive sqlite3 shell on app/storage/xamxam.db
sqlite3 app/storage/xamxam.db ".tables"
``` ```
--- ---
## Configuration ## Migrations
Database paths are centralized in `config/bootstrap.php`: Migrations live in `app/migrations/`. They are run via
`app/migrations/run.php` (wrapped by `just migrate`) and tracked in the
`_migrations` table. Apply the same way in production with
`just deploy-migrate`.
- **Development**: `APP_ROOT . '/storage/test.db'` (gitignored) Each migration is a numbered file:
- **Production**: `APP_ROOT . '/storage/posterg.db'`
The `Database` class (`src/Database.php`) auto-detects: if `test.db` exists → use it, otherwise → use `posterg.db`. Override with `DB_ENV` env var (`test` or `prod`) or pass a custom path to the constructor.
---
## Schema Overview
### Entity Relationship
``` ```
authors ──1:N──► thesis_authors ──N:1──► theses app/migrations/applied/
supervisors ──1:N──► thesis_supervisors ──N:1──► theses ├── 001_add_objet_column.sql
keywords ──1:N──► thesis_keywords ──N:1──► theses ├── …
languages ──1:N──► thesis_languages ──N:1──► theses ├── 041_combined_duration.sql
format_types ──1:N──► thesis_formats ──N:1──► theses ├── 042_fix_contact_columns.sql
orientations ──N:1──► theses └── 043_relabel_promoteur_ulb.php # PHP migrations can run logic too
ap_programs ──N:1──► theses
finality_types ──N:1──► theses
access_types ──N:1──► theses
license_types ──N:1──► theses
thesis_files ──N:1──► theses
``` ```
### Table Categories `schema.sql` is the fully-migrated baseline; it is regenerated from the local
DB so it always reflects the applied set of migrations.
| Category | Tables |
|----------|--------|
| **Core** | `theses`, `authors`, `supervisors`, `thesis_files`, `pages` |
| **Lookup** | `orientations` (15), `ap_programs` (4), `finality_types` (3), `languages` (2+), `format_types` (7), `access_types` (3), `license_types`, `keywords` (dynamic) |
| **Junction** | `thesis_authors`, `thesis_supervisors`, `thesis_keywords`, `thesis_languages`, `thesis_formats` |
| **Views** | `v_theses_full` (admin), `v_theses_public` (published only) |
--- ---
## Core Tables ## Tables
### `theses` 31 tables. Grouped by purpose:
| Column | Type | Required | Description | ### Core
|--------|------|----------|-------------|
| `id` | INTEGER PK | auto | Primary key |
| `identifier` | TEXT UNIQUE | no | Human-readable ID (e.g., "2025-002") |
| `title` | TEXT | **yes** | Thesis title |
| `subtitle` | TEXT | no | Optional subtitle |
| `year` | INTEGER | **yes** | Academic year |
| `is_doctoral` | BOOLEAN | no | 0=TFE, 1=Doctoral |
| `orientation_id` | INTEGER FK | no | → `orientations` |
| `ap_program_id` | INTEGER FK | no | → `ap_programs` |
| `finality_id` | INTEGER FK | no | → `finality_types` |
| `synopsis` | TEXT | no | ~200 word summary |
| `context_note` | TEXT | no | Jury president note (max 150 words) |
| `remarks` | TEXT | no | Internal remarks |
| `duration_minutes` | INTEGER | no | For audio/video |
| `duration_pages` | INTEGER | no | For written works |
| `file_size_info` | TEXT | no | Free-form size description |
| `access_type_id` | INTEGER FK | no | → `access_types` |
| `license_id` | INTEGER FK | no | → `license_types` |
| `jury_points` | DECIMAL(4,2) | no | Grade (0–20) |
| `jury_note_added` | BOOLEAN | no | Jury context note flag |
| `submitted_at` | DATETIME | no | Student submission |
| `defense_date` | DATETIME | no | Defense date |
| `published_at` | DATETIME | no | Publication date |
| `is_published` | BOOLEAN | no | Publication status |
| `baiu_link` | TEXT | no | Institutional repository link |
| `created_at` | DATETIME | auto | Record creation |
| `updated_at` | DATETIME | auto | Last update (trigger) |
**Indexes:** `idx_theses_year`, `idx_theses_published`, `idx_theses_identifier`, `idx_theses_orientation`, `idx_theses_ap_program`, `idx_theses_access_type` | Table | Purpose |
|-------|---------|
| `theses` | The main entity (a TFE or doctoral work) |
| `authors` | People who authored a thesis |
| `supervisors` | Jury members (promoteur / lecteur / président) |
| `thesis_files` | Uploaded files attached to a thesis |
| `theses` subtypes | via `objet` column (`tfe`, `these`, …) and `is_doctoral` |
### `authors` ### Lookup / reference
| Column | Type | Description | | Table | Notes |
|--------|------|-------------| |-------|-------|
| `id` | INTEGER PK | Auto | | `orientations` (15) | Arts Numériques, Dessin, …, Gravure |
| `name` | TEXT NOT NULL | Full name | | `ap_programs` (5) | Narration Spéculative, DPM, APS, LIENS, PACS |
| `email` | TEXT | Contact email (optional) | | `finality_types` (3) | Approfondie, Enseignement, Spécialisée |
| `created_at` / `updated_at` | DATETIME | Auto timestamps | | `languages` | français, anglais, néerlandais, italian (lowercase; expandable) |
| `format_types` | Audio, Vidéo, Site web, Performance, Objet éditorial, Installation, Autre |
| `access_types` (3) | Libre / Interne / Interdit |
| `license_types` | CC BY 4.0 …, Domaine public, Tous droits réservés |
| `tags` | Keywords (dynamic, lowercase-normalised, max 10/thesis) |
**Index:** `idx_authors_email` ### Junction (many-to-many)
### `supervisors` `thesis_authors`, `thesis_supervisors`, `thesis_languages`,
`thesis_formats`, `thesis_tags`.
| Column | Type | Description | ### Sharing / access
|--------|------|-------------|
| `id` | INTEGER PK | Auto |
| `name` | TEXT NOT NULL | Full name |
| `created_at` / `updated_at` | DATETIME | Auto timestamps |
### `thesis_files` | Table | Purpose |
|-------|---------|
| `share_links` | Student/partage submission links (slug, password, expiry, archived) |
| `file_access_requests` | Requests to access restricted files |
| `file_access_tokens` | One-time access tokens |
| `file_access_sessions` | Session tokens for granted access |
| `file_access_audit` | Audit trail of access events |
| Column | Type | Description | ### Integrations / settings / content
|--------|------|-------------|
| `id` | INTEGER PK | Auto |
| `thesis_id` | INTEGER FK | → `theses` (CASCADE) |
| `file_type` | TEXT | `main`, `annex`, `written_part`, `other` |
| `file_path` | TEXT | Relative path |
| `file_name` | TEXT | Original filename |
| `file_size` | INTEGER | Size in bytes |
| `mime_type` | TEXT | MIME type |
| `description` | TEXT | Optional |
| `uploaded_at` | DATETIME | Upload timestamp |
### `pages` | Table | Purpose |
|-------|---------|
| `peertube_settings` | PeerTube video-hosting config (instance, channel, labels) |
| `smtp_settings` | SMTP relay config + notify email (singleton `id=1`) |
| `site_settings` | Feature flags (access types, objets, upload, admin password hash) |
| `pages` | Editable content pages (`about`, `charte`, `licenses`) |
| `apropos_contents` | Structured contact / about content |
| `form_help_blocks` | Help texts on admin/partage forms |
| `system_cache` | Key/value cache |
| Column | Type | Description | ### Auditing
|--------|------|-------------|
| `id` | INTEGER PK | Auto |
| `slug` | TEXT UNIQUE | URL identifier |
| `title` | TEXT NOT NULL | Page title |
| `content` | TEXT | Markdown/HTML |
| `is_published` | BOOLEAN | Default 1 |
| `created_at` / `updated_at` | DATETIME | Auto timestamps |
**Pre-loaded:** `charte`, `about`, `licenses`, `contact` | Table | Purpose |
|-------|---------|
| `admin_audit_log` | Audit log of admin operations (resource, action, status, IP/UA) |
| `audit_log` | Low-level row audit (actor, action, before/after JSON) |
--- ---
## Lookup Tables ## Key columns — `theses`
### `orientations` (15 predefined) | Column | Type | Notes |
|--------|------|-------|
| `id` | INTEGER PK | Auto |
| `identifier` | TEXT | Human-readable id (e.g. `2025-003`) |
| `title` | TEXT NOT NULL | |
| `subtitle` | TEXT | |
| `year` | INTEGER NOT NULL | |
| `is_doctoral` | BOOLEAN | `0`=TFE, `1`=doctoral |
| `objet` | TEXT | `'tfe'` default; other objets (`these`, `frart`) gated by site_settings |
| `orientation_id` / `ap_program_id` / `finality_id` | INT FK | Lookup refs |
| `synopsis` / `context_note` / `remarks` | TEXT | |
| `duration_pages`, `duration_minutes` | INT | Combined duration handled at app level |
| `has_annexes` | BOOLEAN | |
| `access_type_id` | INT FK | Libre / Interne / Interdit |
| `license_id` | INT FK | → `license_types` |
| `cc2r` | BOOLEAN | CC2r licence checkbox |
| `license_custom` | TEXT | Free-form licence text |
| `jury_points` | DECIMAL(4,2) | Grade / 20 |
| `jury_note_added` | BOOLEAN | |
| `contact_visible` | TEXT | `'public'`/`'internal'` control for author contact |
| `submitted_at`, `defense_date`, `published_at` | DATETIME | Lifecycle timestamps |
| `is_published` | BOOLEAN | Public visibility flag |
| `status` | TEXT | Default `'active'` |
| `baiu_link` | TEXT | Institutional repo link |
| `exemplaire_baiu`, `exemplaire_erg` | BOOLEAN | Physical copy flags |
| `deleted_at` | DATETIME | Soft delete |
| `created_at` / `updated_at` | DATETIME | Auto (`updated_at` trigger) |
Arts Numériques, Dessin, Cinéma d'animation, Installation-Performance, Peinture, Photographie, Sculpture, Vidéographie, Graphisme, Typographie, Design Numérique, Illustration, Bande-Dessinée, Sérigraphie, Gravure ### `thesis_supervisors`
### `ap_programs` (4) Tracked via an explicit `role`/flag system rather than separate entity tables:
| Code | Name | | Column | Notes |
|------|------| |--------|-------|
| — | Narration Spéculative | | `role` | `promoteur`, `lecteur`, `president` |
| DPM | Design et Politique du Multiple | | `is_external` | Lecturer is external |
| APS | Atelier Pratiques Situées | | `is_ulb` | Promoteur is from the university (UCLouvain) side |
| LIENS | Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes |
### `finality_types` (3)
Approfondi, Enseignement, Spécialisé
### `format_types` (7)
Site web, Audio, Vidéo, Performance, Objet éditorial, Installation, Autre
### `access_types` (3)
| Name | Description |
|------|-------------|
| Libre | Full access online + library |
| Interne | Physical only; note online |
| Interdit | No access; note only |
**Business rule:** Access can only be restricted (Libre → Interne → Interdit), never opened.
### `languages`
Français, Anglais (expandable)
### `keywords`
Dynamic, grows organically. Max 10 per thesis (application-enforced).
---
## Junction Tables
All use composite PKs (`thesis_id`, `*_id`) with `ON DELETE CASCADE`.
| Table | Links | Order column |
|-------|-------|-------------|
| `thesis_authors` | theses ↔ authors | `author_order` |
| `thesis_supervisors` | theses ↔ supervisors | `supervisor_order` |
| `thesis_keywords` | theses ↔ keywords | — |
| `thesis_languages` | theses ↔ languages | — |
| `thesis_formats` | theses ↔ format_types | — |
**Indexes on junction tables:** `idx_thesis_keywords_thesis`, `idx_thesis_keywords_keyword` (and equivalents for authors)
--- ---
## Views ## Views
### `v_theses_full` — Admin view - **`v_theses_full`** — all theses with joined, human-readable fields
(orientations, AP, finality, access, license, authors, supervisors with
All theses with joined relationships (GROUP_CONCAT for authors, supervisors, keywords, languages, formats, plus human-readable names for orientation, AP, finality, access type, license). role splits, languages, formats, tags/keywords, contact fields).
- **`v_theses_public`** — `v_theses_full` filtered to `is_published = 1`.
### `v_theses_public` — Public view - **`v_smtp_active`** — active SMTP settings row.
Same as `v_theses_full` filtered to `is_published = 1`. Unpublished theses never exposed.
--- ---
## Automatic Features ## Scaffolded / helper tables
- **Auto-increment IDs:** All PKs use `AUTOINCREMENT` - `_migrations` — applied migration bookkeeping (used by the runner).
- **Auto timestamps:** `created_at` defaults to `CURRENT_TIMESTAMP`; `updated_at` refreshed by triggers on UPDATE
- **Cascade deletes:** Deleting a thesis removes all junction + file records
--- ---
## Common Operations ## Business rules
### Querying - **Access:** can only be *restricted* (Libre → Interne → Interdit), never opened back up — enforced in the UI/flow.
- **Keywords (`tags`):** lowercase-normalised, de-duplicated, min 3 (form) / max 10 per thesis.
- **Soft deletes:** reference tables such as `languages` carry `deleted_at`; hard `DELETE` is avoided where recoverability matters.
- **Files:** stored outside the webroot under `app/storage/`; DB rows use storage-relative paths (`theses/<year>/<year>_<AUTHOR>/…`).
---
## Common operations
```sql ```sql
-- Published theses -- Published theses
SELECT * FROM v_theses_public ORDER BY year DESC; SELECT * FROM v_theses_public ORDER BY year DESC;
-- Single thesis (admin) -- Single thesis (admin view)
SELECT * FROM v_theses_full WHERE id = ?; SELECT * FROM v_theses_full WHERE id = ?;
-- By year + orientation -- By year + orientation
SELECT * FROM v_theses_public WHERE year = 2025 AND orientation = 'Arts Numériques'; SELECT * FROM v_theses_public WHERE year = 2025 AND orientation = 'Arts Numériques';
-- By keyword -- Search by keyword (tag)
SELECT DISTINCT t.* FROM theses t SELECT DISTINCT t.* FROM theses t
JOIN thesis_keywords tk ON t.id = tk.thesis_id JOIN thesis_tags tt ON t.id = tt.thesis_id
JOIN keywords k ON tk.keyword_id = k.id JOIN tags g ON tt.tag_id = g.id
WHERE k.keyword = 'écologie' AND t.is_published = 1; WHERE g.name = 'écologie' AND t.is_published = 1;
-- Theses per year -- Theses per year
SELECT year, COUNT(*) FROM theses WHERE is_published = 1 GROUP BY year ORDER BY year DESC; SELECT year, COUNT(*) FROM theses WHERE is_published = 1 GROUP BY year ORDER BY year DESC;
-- Unpublished (admin) -- Publish a thesis
SELECT identifier, title, submitted_at FROM theses
WHERE submitted_at IS NOT NULL AND is_published = 0 ORDER BY submitted_at DESC;
```
### Inserting
```sql
INSERT INTO authors (name, email) VALUES ('Marie Dupont', 'marie@example.com');
INSERT INTO theses (identifier, title, year, orientation_id, finality_id, synopsis)
VALUES ('2026-001', 'Mon Titre', 2026, 8, 1, 'Synopsis...');
INSERT INTO thesis_authors (thesis_id, author_id, author_order) VALUES (1, 5, 1);
INSERT OR IGNORE INTO keywords (keyword) VALUES ('performance');
INSERT INTO thesis_keywords (thesis_id, keyword_id)
SELECT 1, id FROM keywords WHERE keyword = 'performance';
```
### Updating
```sql
UPDATE theses SET is_published = 1, published_at = CURRENT_TIMESTAMP WHERE id = 5; UPDATE theses SET is_published = 1, published_at = CURRENT_TIMESTAMP WHERE id = 5;
UPDATE theses SET jury_points = 16.5, context_note = '…', jury_note_added = 1 WHERE id = 5; ```
> Note: keyword/contact fields in `v_theses_*` are `GROUP_CONCAT` aggregates —
> prefer joining the junction tables (`thesis_tags`/`tags`) when you need
> per-row access.
---
## Backup & maintenance
```bash
just backup # SQL dump → app/storage/backup_<timestamp>.sql
just backup-snapshot # WAL-safe hot backup + gzip → app/storage/backups/
```
On the server, backups are scheduled to `/var/backups/xamxam/` via
`/etc/cron.d/xamxam-backup` (see [deployment.md](deployment.md)).
Manual maintenance:
```bash
sqlite3 app/storage/xamxam.db "VACUUM;" # after large deletes
sqlite3 app/storage/xamxam.db "ANALYZE;"
sqlite3 app/storage/xamxam.db "PRAGMA integrity_check;" # expect "ok"
``` ```
--- ---
## Backup & Maintenance ## Schema changes
### Backup 1. Back up first: `cp app/storage/xamxam.db app/storage/xamxam.db.bak`
2. Add a numbered migration file under `app/migrations/applied/`
(`.sql` for DDL/DML, `.php` when data logic is needed).
3. Run `just migrate` (or `just deploy-migrate` on the server) — idempotent,
tracked via `_migrations`.
4. When appropriate, regenerate `app/storage/schema.sql` from the applied set
so it stays a faithful baseline.
```bash Change request format:
# File copy (simplest)
cp posterg.db backups/posterg_$(date +%Y%m%d).db
# SQL dump (portable)
sqlite3 posterg.db .dump > backups/posterg_$(date +%Y%m%d).sql
```
### Maintenance
```bash
sqlite3 posterg.db "VACUUM;" # Reclaim space (after large deletes, monthly)
sqlite3 posterg.db "ANALYZE;" # Update query stats (after schema/data changes)
sqlite3 posterg.db "PRAGMA integrity_check;" # Verify → should output "ok"
sqlite3 posterg.db "PRAGMA journal_mode=WAL;" # Enable WAL for better concurrency
```
### Recovery
```bash
sqlite3 posterg.db ".recover" | sqlite3 recovered.db # Corrupted DB
sqlite3 posterg.db .dump | sqlite3 new.db # Dump + reimport
```
---
## Performance Notes
- All critical foreign keys and search fields are indexed
- Views pre-compute joins for common queries
- For 1000+ theses: ensure WAL mode, run `ANALYZE` periodically, consider `VACUUM`
- Cache size: `PRAGMA cache_size=-64000;` (64MB)
- Memory-mapped I/O: `PRAGMA mmap_size=268435456;` (256MB)
---
## Schema Changes
### Making changes
1. Always backup first: `cp posterg.db posterg_before.db`
2. Test on backup: `sqlite3 posterg_test.db < migration.sql`
3. Use transactions: wrap ALTER/INSERT in `BEGIN; … COMMIT;`
4. Document in `storage/migrations/` with numbered SQL files
### Change request format
``` ```
Table: [table_name] Table: [table_name]
+145 -148
View File
@@ -1,204 +1,201 @@
# Deployment # Deployment
Server setup, deployment, and rollback procedures for Post-ERG. Server setup, deployment, backups, and rollback procedures for XAMXAM.
--- ---
## One-Time Server Setup ## Overview
Run before first deploy: - Production host: `xamxam` (over SSH), app root `/var/www/xamxam/`
- Files are pushed with `rsync` — **there is no git repo on the remote**
- Web / FPM user: `www-data`, app group: `xamxam`
- The **DocumentRoot** is `app/public/`, but on the server the code lives flat
under `/var/www/xamxam/` (Composer autoload path adjusted to `src/`)
- SQLite database at `/var/www/xamxam/storage/xamxam.db`
Deployment is orchestrated through the `justfile` (`deploy` group).
---
## One-time server setup
```bash ```bash
just setup-server ssh xamxam
sudo mkdir -p /var/www/xamxam
sudo chown www-data:xamxam /var/www/xamxam
sudo chmod 775 /var/www/xamxam
exit
``` ```
This creates `/var/www/posterg/` with correct ownership/permissions: Then from local, deploy once and apply the nginx config + verify permissions:
- Owner: `www-data:posterg`
- Directories: **2775** (setgid — new files inherit `posterg` group)
- Files: **664**
- Database files: **660**
> **Important:** After running `setup-server`, log out and back in on the server (or `newgrp posterg`) so group membership is active before deploying. ```bash
just deploy
just deploy-nginx
```
### Why setgid (2775)? For a full initial rollout including backup + cleanup cron jobs:
rsync uses `--chown=www-data:posterg`. Both `padlock` and `www-data` must write to dirs. With `2775 + group=posterg`, new subdirs inherit the group automatically. ```bash
just deploy-all-first # deploy + deploy-backup + deploy-cleanup-cron
```
--- ---
## Deploying ## Deploying
| Command | Purpose |
|---------|---------|
| `just deploy` | Full deploy: build + code + Composer deps + migrations + env + permissions check |
| `just deploy-code` | rsync app files + nginx config + permissions (no Composer, no migrations) |
| `just deploy-deps` | Sync composer.{json,lock} → server, then `composer install`/`dump-autoload` |
| `just deploy-migrate` | Run pending DB migrations on the server |
| `just deploy-env` | Upload `app/.env` (only if the remote `.env` is absent — never overwrites a key) |
| `just deploy-nginx` | Upload + apply + reload nginx config |
| `just deploy-db` | Push local `xamxam.db` → remote (**refuses** if a remote DB already exists) |
| `just deploy-verify-permissions` | Check ownership / permissions on the server |
> ℹ️ **First deploy?** After `just deploy`, run `just deploy-backup` to install
> the backup script + cron jobs.
### Environment file & re-encryption
`app/.env` holds secrets (e.g. `APP_KEY`). `deploy-env` will **not** overwrite a
remote `.env` that already has an `APP_KEY`. If you rotate `APP_KEY`, re-encrypt
the SMTP password and push the new key:
```bash ```bash
just deploy # Push all app files just reencrypt-password <new_base64_key> # runs scripts/reencrypt-smtp-password.php on server
just deploy-db # Push initial database (aborts if remote DB exists) just deploy-env
just deploy-nginx # Push + apply nginx config
``` ```
### First-Time Deployment ---
Since we moved from `/var/www/html/` to `/var/www/posterg/`: ## Backups
SQLite backups are taken with a WAL-safe hot backup (`sqlite3 .backup`) then
gzipped to `/var/backups/xamxam/` on the server.
| Command | Purpose |
|---------|---------|
| `just deploy-backup` | Install backup script + cron jobs (one-shot) |
| `just deploy-backup-script` | Install `/usr/local/bin/backup-sqlite.sh` |
| `just deploy-backup-cron` | Install `/etc/cron.d/xamxam-backup` (hourly 30d + daily 90d) + dirs/log |
| `just deploy-check-backup-log` | Tail `/var/log/sqlite-backup.log` |
| `just deploy-list-backups` | List backups on the server |
| `just trigger-backup` | Run the backup script now |
| `just test-restore <path.gz>` | Fetch + decompress + verify a remote snapshot |
Retention: hourly backups kept 30 days, nightly (02:00) backups kept 90 days.
Backup files: `/var/backups/xamxam/db-<timestamp>.db.gz`.
Draft cleanup is handled by a separate cron (`/etc/cron.d/xamxam-cleanup`),
installed via `just deploy-cleanup-cron`, logging to
`/var/log/xamxam-cleanup.log`. Verify with `just deploy-check-cleanup-log`.
---
## Permissions model
Managed/simulated by the justfile and verified by `deploy-verify-permissions`:
- Ownership: `www-data:xamxam`
- Directories: **2775** (setgid — new files inherit the `xamxam` group)
- Regular files: **664**
- `storage/xamxam.db` and other `*.db`: **660**
- `app/.env`: **640**
The nginx/`deploy-server.sh` step (`just deploy-nginx`, or
`sudo DEPLOY_USER=$USER bash /tmp/deploy-server.sh` via `just deploy-script`)
fixes permissions and installs the nginx config.
---
## Storage migration
If paths moved (e.g. legacy upload locations), a one-off migration script can
rewrite stored paths on the server:
1. **Setup server directory** (one time):
```bash ```bash
just setup-server just deploy-migrate-storage # apply
just deploy-migrate-storage --dry-run # dry-run only
``` ```
2. **Deploy application**: ---
```bash
just deploy ## Rollback
```
Uploads to `/var/www/posterg/`, excludes tests/docs/vendor. Because there is no repo on the remote, rollback means restoring files and/or
restoring the database from a backup.
### Restore the database
3. **Deploy nginx config**:
```bash ```bash
just deploy-nginx ssh xamxam
ssh posterg sudo systemctl stop nginx
sudo bash /tmp/deploy-production.sh cp /var/backups/xamxam/db-<timestamp>.db.gz /tmp/restore.db.gz
sudo systemctl reload nginx gunzip -c /tmp/restore.db.gz > /var/www/xamxam/storage/xamxam.db
chown www-data:xamxam /var/www/xamxam/storage/xamxam.db
chmod 660 /var/www/xamxam/storage/xamxam.db
sudo systemctl start nginx
``` ```
4. **Verify**: ### Restore application files
```bash
just server-status
curl -I https://posterg.erg.be/ # 200 ✓
curl -I https://posterg.erg.be/admin/ # 200 ✓
curl -I https://posterg.erg.be/storage/ # 404 ✓
```
### Subsequent Deployments Re-run `just deploy` from a good local state. In jj you can jump back to a
previous commit and redeploy:
```bash ```bash
jj log # find a known-good change
jj edit <previous-change-id> # work from that revision
just deploy just deploy
``` ```
--- ---
## Server Directory Structure ## Verify after deploy
```
/var/www/posterg/ # Application root (private)
├── public/ # DocumentRoot (nginx points here)
│ ├── index.php
│ ├── search.php
│ ├── memoire.php
│ ├── admin/
│ └── assets/
├── includes/ # Templates (private)
├── config/ # Configuration (private)
├── storage/ # Database + uploads (private)
│ ├── posterg.db
│ └── theses/
├── src/ # PHP classes (private)
└── scripts/ # Admin tools (private)
```
**Nginx DocumentRoot:** `/var/www/posterg/public/`
Only `public/` is web-accessible. Everything else is physically private.
---
## Managing Admin Users
```bash ```bash
ssh posterg "sudo bash /var/www/posterg/scripts/manage-admin-users.sh" curl -I https://xamxam.erg.be/ # expect 200
``` curl -I https://xamxam.erg.be/admin/ # expect 200
curl -I https://xamxam.erg.be/storage/ # expect 404 (blocked)
Interactive menu for adding/changing/deleting htpasswd entries at `/etc/nginx/.htpasswd-posterg`. just deploy-verify-permissions # expect "All permissions OK"
---
## Security Verification
After every deploy, verify private files are inaccessible:
```bash
curl -I https://posterg.erg.be/storage/test.db # Must 404
curl -I https://posterg.erg.be/config/bootstrap.php # Must 404
curl -I https://posterg.erg.be/src/Database.php # Must 404
``` ```
--- ---
## Troubleshooting ## Troubleshooting
### rsync Permission Denied - **502 Bad Gateway**
```bash ```bash
just setup-server # Fixes directory permissions sudo systemctl status php8.4-fpm
# Then log out/in on server and retry sudo systemctl restart php8.4-fpm
``` ```
- **Nginx config error**
Manual fix:
```bash ```bash
ssh posterg sudo nginx -t
sudo chown -R www-data:posterg /var/www/posterg
sudo find /var/www/posterg -type d -exec chmod 2775 {} \;
sudo find /var/www/posterg -type f -exec chmod 664 {} \;
sudo chmod 660 /var/www/posterg/storage/*.db
``` ```
- **`deploy-db` refuses to run**
### Nginx 403 Forbidden The remote DB already exists. Remove it manually only if you intend to
overwrite production data.
```bash - **Composer did not pick up new classes**
ssh posterg `deploy-deps` runs `composer dump-autoload` when the lock checksum is
sudo find /var/www/posterg -type d -exec chmod 2775 {} \; unchanged; if that still fails, force a reinstall.
sudo find /var/www/posterg -type f -exec chmod 664 {} \; - **Backup/schedule not running**
sudo chmod 660 /var/www/posterg/storage/*.db Confirm the cron files are installed (`/etc/cron.d/xamxam-backup`,
``` `/etc/cron.d/xamxam-cleanup`) and the log files are writable by `www-data`.
### Database Permission Error
```bash
ssh posterg
sudo chown www-data:posterg /var/www/posterg/storage/posterg.db
sudo chmod 660 /var/www/posterg/storage/posterg.db
```
### Nginx 500 / Site 404
Check nginx DocumentRoot:
```bash
ssh posterg "grep 'root ' /etc/nginx/sites-available/posterg"
# Should show: root /var/www/posterg/public;
```
### Admin 404
Nginx may still use old `/formulaire/` location. Update `nginx/posterg.conf` to use `/admin/`.
--- ---
## Rollback ## Selected commands reference
If something goes wrong:
```bash
# Restore old nginx config
ssh posterg
sudo cp /etc/nginx/sites-available/posterg.backup /etc/nginx/sites-available/posterg
sudo systemctl reload nginx
# Or restore old site (if backed up)
sudo rm -rf /var/www/posterg
sudo mv /var/www/html.backup /var/www/html
```
With jj:
```bash
jj log
jj edit <previous-change-id>
```
---
## Commands Reference
| Command | Purpose | | Command | Purpose |
|---------|---------| |---------|---------|
| `just setup-server` | Create `/var/www/posterg/` (first time only) | | `just deploy` | Full deploy (build + code + deps + migrate + env + perms) |
| `just deploy` | Deploy application files | | `just deploy-nginx` | Apply nginx config |
| `just deploy-nginx` | Update nginx configuration | | `just deploy-backup` | Install backup script + cron |
| `just deploy-db` | Deploy database file | | `just deploy-cleanup-cron` | Install orphaned-draft cleanup cron |
| `just server-status` | Check server health | | `just deploy-list-backups` | List server backups |
| `just server-logs` | View server logs | | `just trigger-backup` | Run backup now |
| `just reencrypt-password <key>` | Re-encrypt SMTP password after key rotation |
| `just test-restore <path.gz>` | Verify a snapshot |
+113 -227
View File
@@ -1,272 +1,158 @@
# Development Guide # Development Guide
Setup, workflow, testing, and live reload for Post-ERG development. Setup, workflow, building assets, and testing for XAMXAM development.
--- ---
## Quick Start ## Requirements
- **PHP** ≥ 8.4 with `ext-json`, `ext-openssl`, `ext-pdo` (and `ext-sqlite3` for the local DB)
- **Composer** for PHP dependencies
- **Node.js / npm** for frontend asset builds (rolldown, lightningcss, biome)
- **SQLite3** CLI (for `just query`, `just init-db`)
## One-time setup
From the repo root, install dependencies (manually — there is no `just` recipe
for these):
```bash ```bash
just setup # One time: clone php-live-reload + setup directories composer install # PHP deps (vendor/)
just serve # Start dev server at http://localhost:8000 npm ci # JS build deps (node_modules/)
just migrate # create/update the SQLite DB from schema + migrations
``` ```
One unified server serves both: `just setup` exists but its backing script (`scripts/setup-dev.sh`) is **stale**
- **Public site:** http://localhost:8000 — it still clones the old `php-live-reload` library and creates legacy
- **Admin panel:** http://localhost:8000/admin/ `admin/data/` directories. Live-reload now ships inside the app
(`app/public/live-reload.php`), and assets are built with rolldown/lightningcss,
Live reload is enabled automatically — browser refreshes when you save files. not the live-reload watcher. Prefer the explicit `composer install` + `npm ci`
above.
---
## Project Structure ## Project Structure
``` ```
posterg-website/ xamxam/
├── public/ # DocumentRoot (web-accessible) ├── app/ # All application code (the project root on the server)
│ ├── index.php # Homepage │ ├── bootstrap.php # App bootstrapping (constants, autoload, config)
│ ├── search.php # Search/répertoire │ ├── router.php # Router for the PHP built-in dev server
│ ├── memoire.php # Thesis detail │ ├── public/ # DocumentRoot (web-accessible only)
│ ├── admin/ # Admin panel │ │ ├── index.php # Front controller / entry point
│ └── assets/ # CSS, fonts, images │ │ ├── request-access.php
├── includes/ # Template partials (header, footer) │ │ ├── live-reload.php
├── config/ # Configuration (bootstrap.php) │ │ ├── admin/ # Admin panel
├── src/ # PHP classes (Database, AdminAuth, RateLimit) │ │ ├── partage/ # Student submission via share links
├── storage/ # Database + uploads (private) │ │ └── assets/ # Built CSS/JS, fonts, images (dist/ is generated)
├── database/ # Schema + migrations │ ├── src/ # PHP classes
├── tests/ # Test suite │ │ ├── Controllers/ # Request controllers (Home, Search, Tfe, Export, …)
├── nginx/ # Server configuration │ │ ├── Form/ # Form helper
├── scripts/ # Deployment/admin scripts │ │ ├── Database.php # DB access + queries
└── vendor/ # Third-party (gitignored, dev only) │ │ ├── AdminAuth.php # Session auth for admin
│ │ ├── RateLimit.php
│ │ ├── FilepondHandler.php
│ │ ├── PeerTubeService.php
│ │ └── … # (see list in src/)
│ ├── templates/ # Template partials (public, admin, partials, partage)
│ ├── storage/ # Database + uploads (private)
│ │ ├── xamxam.db # SQLite database
│ │ ├── schema.sql # Base schema
│ │ ├── theses/ tfe/ # Uploaded files
│ │ └── cache/ logs/ # Runtime data
│ └── migrations/ # Migration runner + applied migrations
├── scripts/ # Build / deploy / utility scripts
├── tests/phpunit/ # PHPUnit tests
├── nginx/ # Server configuration + docs
├── deploy/ # Cron configs (backup, cleanup)
├── justfile # Task runner
├── composer.json / package.json
└── TODO.md
``` ```
---
## Development Workflow ## Development Workflow
### Starting Development ### Start the dev server
```bash ```bash
just serve just dev
``` ```
### Making Changes This runs a one-shot asset build, applies pending migrations, opens
`http://127.0.0.1:8000/` (public) and `/admin/` (admin), then starts the PHP
built-in server (`php -S 127.0.0.1:8000`) with a `chokidar` watcher that
rebuilds CSS/JS on change.
1. Edit PHP/CSS files — browser auto-refreshes - **Public site:** `http://127.0.0.1:8000/`
2. Run tests: `just test` - **Admin panel:** `http://127.0.0.1:8000/admin/`
3. Check syntax: `just syntax`
4. Deploy: `just deploy`
### Database Operations Stop it with `just stop`.
If you only need the server without browsers/ui, run `just dev-build` first,
then start the PHP built-in server manually with the `php -d … -S` command
shown in the `dev` recipe (or add a `just serve` alias locally).
### Frontend assets
Source CSS lives in `app/public/assets/css/`, source JS in
`app/public/assets/js/app/`. They are **bundled and minified** into
`app/public/assets/dist/` by `scripts/build*.mjs` (rolldown + lightningcss).
```bash ```bash
just stats # View database stats just dev-build # one-shot build (quicker output than full)
just query # Open SQLite shell just build # full build
just show 42 # Show thesis by ID just build-css # CSS only
just reset-db # Reset database just build-js # JS only
just fixtures # Create sample data just build-check # verify build output is up to date
just backup # Backup database
``` ```
--- There is no live-reloading in production — `app/public/live-reload.php` only
activates under the PHP built-in server.
## Live Reload
### What It Does
Automatically refreshes your browser when you save PHP/CSS/JS files. No browser extension needed.
### Setup (One Time)
```bash
just setup
```
Clones `php-live-reload` into `vendor/` (gitignored).
### How It Works
- Conditionally included in `header.php` when `php_sapi_name() === 'cli-server'`
- JavaScript polls server for file changes → browser refreshes
- **Never active in production** (different SAPI, vendor/ not deployed)
### Detection
```php
<?php if (php_sapi_name() === 'cli-server'): ?>
<script src="/vendor/php-live-reload/php-live-reload/live-reload.js"></script>
<?php endif; ?>
```
### Troubleshooting
```bash
ls -la vendor/php-live-reload/ # Check installed
just setup # Reinstall if missing
curl -s http://localhost:8000/ | grep live-reload # Verify script included
```
---
## Testing ## Testing
### Test Structure PHPUnit is used. Configuration lives in `phpunit.xml`, tests in `tests/phpunit/`
(with `tests/bootstrap.php` and `tests/TestDatabase.php` helpers).
```
tests/
├── run-tests.php # Main test runner
├── Unit/ # Unit tests
│ ├── DatabaseTest.php
│ └── RateLimitTest.php
├── Integration/ # Integration tests
│ └── SearchTest.php
└── Security/ # Security tests
└── SecurityTest.php
```
### Running Tests
```bash ```bash
just test # Run all tests just test # run all PHPUnit tests
just test-unit # Unit tests only just test-coverage # run with HTML coverage into coverage/
just test-integration # Integration tests only
just test-security # Security tests only
just syntax # Check PHP syntax
``` ```
### Writing Tests ### Linting / formatting
1. Choose type: Unit / Integration / Security
2. Create test file in appropriate `tests/` subdirectory
3. Follow template:
```php
<?php
require_once __DIR__ . '/../../src/Database.php';
echo "Test Name\n";
echo "=========\n\n";
try {
$db = Database::getInstance();
echo "✓ PASS: Test description\n";
return true;
} catch (Exception $e) {
echo "❌ FAIL: " . $e->getMessage() . "\n";
return false;
}
```
4. Add to `tests/run-tests.php` `$testFiles` array
5. Run: `just test`
---
## Common Tasks
### Create a New Page
```php
<?php
require_once __DIR__ . '/../config/bootstrap.php';
require_once APP_ROOT . '/src/Database.php';
$db = App::boot();
include APP_ROOT . '/includes/header.php';
?>
<section class="section">
<div class="container">
<h1 class="title">New Page</h1>
</div>
</section>
<?php include APP_ROOT . '/includes/footer.php'; ?>
```
### Add a Database Method
1. Edit `src/Database.php`
2. Add method to the class
3. Write test in `tests/Unit/`
4. Run: `just test-unit`
### Update CSS
1. Edit `public/assets/posterg.css`
2. Browser auto-refreshes
3. Increment cache-bust in header: `posterg.css?v=N`
---
## Justfile Commands
### Development
| Command | Description |
|---------|-------------|
| `just setup` | Setup dev environment (one-time) |
| `just serve` | Start dev server with live reload |
| `just stop` | Stop dev server |
| `just logs` | View dev logs |
### Testing
| Command | Description |
|---------|-------------|
| `just test` | Run all tests |
| `just test-unit` | Unit tests only |
| `just test-integration` | Integration tests only |
| `just test-security` | Security tests only |
| `just syntax` | Check PHP syntax |
### Database
| Command | Description |
|---------|-------------|
| `just stats` | Database statistics |
| `just query` | Open SQLite shell |
| `just show <id>` | Show thesis by ID |
| `just reset-db` | Reset test database |
| `just fixtures` | Create sample data |
| `just backup` | Backup database |
### Deployment
| Command | Description |
|---------|-------------|
| `just deploy` | Deploy complete site |
| `just deploy-nginx` | Deploy nginx config |
| `just deploy-db` | Deploy database |
| `just server-status` | Check server health |
| `just server-logs` | View server logs |
---
## Debugging
```bash ```bash
just logs # View error logs just lint-php # phpstan (static analysis) + php-cs-fixer (coding standards)
tail -f error.log # Direct log monitoring just lint-css # biome lint on app/public/assets/css/
just lint-js # biome lint on app/public/assets/js/app/ + scripts/
# PHP errors in browser (temporary): just lint # all linters
# Add to PHP file: just fix # auto-fix (biome + php-cs-fixer)
ini_set('display_errors', 1);
error_reporting(E_ALL);
# Database issues:
just stats # Check DB exists and has data
just query # Open SQLite shell
``` ```
### Server Won't Start ## Database Operations
```bash ```bash
just stop # Kill existing process just migrate # run pending migrations
just setup # Reinstall php-live-reload just init-db # create/reset DB from app/storage/schema.sql
just reset-db # rm DB + init-db
just query # open an interactive SQLite shell
just backup # SQL dump into app/storage/backup_<timestamp>.sql
just backup-snapshot # WAL-safe hot backup + gzip into storage/backups/
just cleanup-drafts [--no-dry-run] # remove orphaned drafts > 24h
``` ```
### Database Errors See [database.md](database.md) for the full schema reference.
## Deployment
See [deployment.md](deployment.md). Files are pushed to the server with
`just deploy` (rsync → `xamxam:/var/www/xamxam/`). There is no git repo on
the remote.
## Troubleshooting
```bash ```bash
just reset-db # Reset from schema just logs # tail the dev error log (error.log)
just fixtures # Repopulate with sample data just stop # kill the dev server / asset watcher
just test # Verify everything works
``` ```
If the browser doesn't hot-reload, confirm the asset watcher is still running
and the rebuild succeeded (`just dev-build`).
+6 -4
View File
@@ -106,7 +106,7 @@ sudo systemctl stop nginx
# Remplacer le fichier SQLite # Remplacer le fichier SQLite
cp xamxam-db-AAAA-MM-JJ.sqlite /var/www/xamxam/storage/xamxam.db cp xamxam-db-AAAA-MM-JJ.sqlite /var/www/xamxam/storage/xamxam.db
chown www-data:www-data /var/www/xamxam/storage/xamxam.db chown www-data:www-data /var/www/xamxam/storage/xamxam.db
chmod 640 /var/www/xamxam/storage/xamxam.db chmod 660 /var/www/xamxam/storage/xamxam.db
``` ```
### 2. Restaurer les fichiers ### 2. Restaurer les fichiers
@@ -183,9 +183,11 @@ d'exécution raisonnable (`max_execution_time ≥ 60s`) est recommandé.
### Colonnes du CSV d'export ### Colonnes du CSV d'export
Le CSV utilise la même structure que l'import (21 colonnes). Il est Le CSV utilise les mêmes colonnes que l'import (les en-têtes proviennent de
directement ré-importable sans modification. Voir [`docs/import.md`](import.md) `ExportController::CSV_HEADERS`, 26 colonnes dont `CC2r`, `Exemplaire BAIU` et
pour le détail des colonnes. `Exemplaire ERG`). Il est directement ré-importable — l'import ignore les
colonnes purement d'export. Voir [`docs/import.md`](import.md) pour le détail
des colonnes et de la logique d'import.
--- ---
+27 -26
View File
@@ -93,35 +93,34 @@ Files whose MIME type is `application/octet-stream` are accepted **only if their
Max size: **20 MB**. Max size: **20 MB**.
### Banner image (`banner` input) ### Banner image
| Extension | MIME type | > Removed — the home-page banner was merged into covers (migration
|-----------|-----------| > `028_drop_banner_path.sql`). There is no separate banner upload anymore.
| `.jpg` / `.jpeg` | `image/jpeg` |
| `.png` | `image/png` |
| `.webp` | `image/webp` |
Landscape format recommended (4:1 ratio). Max size: **20 MB**.
--- ---
## Size limits ## Size limits
| Limit | Value | Per-field limits are enforced in `app/src/Controllers/validate-file-fragment-shared.php`
|-------|-------| (server-side `finfo` + size check on every upload):
| Per-file limit (TFE content files) | **500 MB** |
| PHP `upload_max_filesize` | 512 MB |
| PHP `post_max_size` | 520 MB |
| Cover image | 20 MB |
| Banner image | 20 MB |
The PHP limits are set in: | Field | Max size | Accepted content |
|-------|----------|------------------|
| `tfe` (main files) | **500 MB** default; **PDF capped at 100 MB**; **video/audio up to 5 GB** | PDF, images, video, audio, archives |
| `annexes` | **500 MB** (PDF capped at 100 MB, video/audio up to 5 GB) | PDF, archives, images, media |
| `couverture` (cover) | **20 MB** | JPG / PNG / WEBP |
| `note_intention` | **100 MB** | PDF |
| File | Applies to | (Admins bypass validation entirely — `admin_mode=1`.)
|------|------------|
| `app/public/.htaccess` | Apache (`mod_php`) | The PHP engine limits are set as follows:
| `app/public/.user.ini` | PHP-FPM / nginx |
| `justfile` — `serve` recipe | PHP built-in dev server (`php -S` ignores both files above, so limits are passed via `-d` flags) | | File | Applies to | `upload_max_filesize` | `post_max_size` |
|------|------------|----------------------|------------------|
| `app/public/.user.ini` | PHP-FPM / nginx | 8192M | 8704M |
| `app/public/.htaccess` | Apache (`mod_php`) | 512M | 1024M |
| `justfile` — `dev` recipe | PHP built-in dev server (`php -S` ignores the files above, so limits are passed via `-d` flags) | 8192M | 8704M |
For environments that require different limits, edit all three. For environments that require different limits, edit all three.
@@ -160,8 +159,6 @@ Files are stored outside the webroot in `app/storage/`.
app/storage/ app/storage/
├── covers/ ├── covers/
│ └── <random-hex>.jpg # cover images │ └── <random-hex>.jpg # cover images
├── banners/
│ └── <random-hex>.jpg # home-page banners
└── theses/ └── theses/
└── <year>/ └── <year>/
└── <YEAR>_<AUTHOR_SLUG>/ └── <YEAR>_<AUTHOR_SLUG>/
@@ -203,8 +200,9 @@ CREATE TABLE thesis_files (
file_size INTEGER, -- bytes file_size INTEGER, -- bytes
mime_type TEXT, mime_type TEXT,
description TEXT, -- legacy caption field description TEXT, -- legacy caption field
display_label TEXT, -- per-file caption (migration 007) display_label TEXT, -- per-file caption
sort_order INTEGER NOT NULL DEFAULT 0, -- display order (migration 007) sort_order INTEGER NOT NULL DEFAULT 0, -- display order
file_hash TEXT, -- stored hash (optional)
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE FOREIGN KEY (thesis_id) REFERENCES theses(id) ON DELETE CASCADE
); );
@@ -225,7 +223,10 @@ Files are queried ordered by `sort_order ASC, uploaded_at ASC`.
| `app/templates/partials/form/fieldset-files.php` | Upload UI partial (add / partage forms) | | `app/templates/partials/form/fieldset-files.php` | Upload UI partial (add / partage forms) |
| `app/templates/admin/edit.php` | Edit-form files section (sortable existing files + new upload queue) | | `app/templates/admin/edit.php` | Edit-form files section (sortable existing files + new upload queue) |
| `app/templates/public/tfe.php` | Public rendering of all file types | | `app/templates/public/tfe.php` | Public rendering of all file types |
| `app/public/assets/js/file-upload-queue.js` | SortableJS-backed upload queue + legacy preview | | `app/src/Controllers/FilepondHandler.php` + `app/public/assets/js/app/file-upload-filepond.js` | FilePond-backed upload queue |
| `app/public/.htaccess` | PHP upload limits (Apache) | | `app/public/.htaccess` | PHP upload limits (Apache) |
| `app/public/.user.ini` | PHP upload limits (PHP-FPM / nginx) | | `app/public/.user.ini` | PHP upload limits (PHP-FPM / nginx) |
| `app/migrations/applied/007_thesis_files_sort_and_label.sql` | DB migration adding `sort_order` + `display_label` | | `app/migrations/applied/007_thesis_files_sort_and_label.sql` | DB migration adding `sort_order` + `display_label` |
> Note: the upload UI was migrated from the legacy SortableJS `file-upload-queue.js`
> to **FilePond** (`file-upload-filepond.js` + `FilepondHandler.php`).
+89 -134
View File
@@ -1,153 +1,108 @@
# CSV Import Format Specification # CSV Import
## File Format Import of theses from the admin panel, plus the CSV structure and behaviour.
- **Encoding**: UTF-8 > **Where it lives:** the CSV import is handled **inline** in
- **Delimiter**: Comma (`,`) > `app/public/admin/index.php` — there is no separate `import.php` action. The
- **Header Rows**: First 4 rows are skipped during import > `/admin/import.php` file only redirects to `/admin/`.
- Row 1: Empty
- Row 2: Headers (French labels)
- Row 3: Description row
- Row 4: Column names
- **Data Rows**: Start from row 5 onwards
## Column Structure ---
The CSV must contain exactly 21 columns in this order: ## File format
| Index | Field Name | Required | Type | Description | - **Encoding:** UTF-8
|-------|------------|----------|------|-------------| - **Delimiter:** comma (`,`), quoting with double quotes (`""` escapes a quote)
| 0 | identifier | No | String | Unique identifier for the thesis | - **Header detection:** the importer scans up to **8 rows** looking for a header
| 1 | title | **Yes** | String | Thesis title | row containing at least **11 recognised column names**. Matching is flexible:
| 2 | subtitle | No | String | Thesis subtitle | prefix/substring/variant matching (e.g. `contact.visible` matches `contact`,
| 3 | authors | No | String | Author(s), comma-separated for multiple | `Licence` ↔ `License`, `Promoteur·ice(s) université` matches `ulb`).
| 4 | contact | No | String | Contact email (associated with first author) | - **No header fallback:** if no header row is found, the importer falls back to
| 5 | supervisors | No | String | Supervisor(s), comma-separated for multiple | **positional** interpretation, skipping the first **4 rows** (the legacy
| 6 | formats | No | String | Format(s), comma-separated for multiple | template layout with label/description rows).
| 7 | year | **Yes** | Integer | Year of thesis (e.g., 2024) |
| 8 | ap | No | String | AP program code (see AP Codes section) |
| 9 | orientation | No | String | Orientation code (see Orientation Codes section) |
| 10 | finality | No | String | Finality name |
| 11 | keywords | No | String | Keywords, comma-separated (max 10) |
| 12 | synopsis | No | Text | Synopsis/abstract of the thesis |
| 13 | context | No | Text | Context note |
| 14 | remarks | No | Text | Additional remarks |
| 15 | language | No | String | Language (e.g., Français, English, Nederlands) |
| 16 | access | No | String | Access authorization |
| 17 | license | No | String | License information |
| 18 | size_info | No | String | File size information |
| 19 | jury_points | No | Float | Jury score (out of 20) |
| 20 | baiu_link | No | String | Link to BAIU (institutional archive) |
## Field Details So import files are robust to column reordering as long as meaningful headers
are present; legacy fixed-position files (skip-4) also still work.
### Required Fields ---
- **title**: Must not be empty
- **year**: Must not be empty and must be a valid integer
### Multi-Value Fields ## Recognised columns
These fields accept multiple values separated by commas:
- **authors**: e.g., `"John Doe, Jane Smith"`
- **supervisors**: e.g., `"Prof. A, Prof. B"`
- **keywords**: Maximum 10 keywords, e.g., `"art, design, digital"`
- **formats**: e.g., `"PDF, Video, Installation"`
### Orientation Codes Headings are matched by the keywords below (case-insensitive). They mirror the
Valid orientation codes and their full names: **export** header names in `ExportController::CSV_HEADERS`.
| Export header (order in export) | Imported as | Notes |
|---|---|---|
| `Identifiant` | identifier | Optional; if it already exists, the row is skipped |
| `Titre` | title | **Required** |
| `Sous-titre` | subtitle | |
| `Auteur·ice(s)` | authors | comma-separated; first author gets `Contact` |
| `Contact` | contact | email; `OUI`/`NON` artefacts emptied |
| `Promoteur·ice(s) interne` | promoteur (interne) | role `promoteur`, `is_ulb=0` |
| `Lecteur·ice(s) interne` | lecteur (interne) | role `lecteur`, `is_external=0` |
| `Lecteur·ice(s) externe` | lecteur (externe) | role `lecteur`, `is_external=1` |
| `Promoteur·ice(s) université` | promoteur (ulb) | role `promoteur`, `is_ulb=1` |
| `Format(s)` | formats | comma-separated; matched to `format_types` |
| `Année` | année | **Required** unless derivable from `Identifiant` (`2024-003` → 2024) |
| `AP` | ap | AP program code or name |
| `Orientation` | orientation | code or full name (see below) |
| `Finalité` | finalité | matched to `finality_types` |
| `Mots-clés` | mots-clés | comma-separated; lowercase-normalised; max 10 |
| `Synopsis` | synopsis | |
| `Contexte` | contexte | context note |
| `Remarques` | remarques | |
| `Langue` | langue | comma-separated; stored lowercase; created if missing |
| `Autorisation` | autorisation | access type name → `access_types`; default `Libre` if unknown |
| `Licence` / `License` | license | licence name |
| `Points sur 20` | points | float (jury points) |
| `Lien BAIU` | lien baiu | institutional link |
| `CC2r`, `Exemplaire BAIU`, `Exemplaire ERG` | *(export only)* | written by export; ignored on import |
> Positional-fallback indices (0-based): `0` Identifier, `1` Titre, `2`
> Sous-titre, `3` Auteur, `4` Contact, `5` Promoteurs, `6` Lecteurs internes,
> `7` Lecteurs externes, `8` Promoteurs ULB, `9` Formats, `10` Année, `11` AP,
> `12` Orientation, `13` Finalité, `14` Mots-clés, `15` Synopsis, `16` Contexte,
> `17` Remarques, `18` Langue, `19` Autorisation, `20` Licence, `21` Points,
> `22` Lien BAIU.
The three trailing **export-only** columns (CC2r / Exemplaire BAIU / Exemplaire
ERG) are not currently read back on import.
---
## Orientation & AP values
Orientation codes (legacy short form) and aliases are normalised to canonical
DB names. Recognised codes:
``` ```
SC = Sculpture AN=Arts Numériques, DE=Dessin, CA=Cinéma d'animation, IP=Installation-Performance,
VI = Vidéographie PE=Peinture, PH=Photographie, SC=Sculpture, VI=Vidéographie, GR=Graphisme,
CA = Cinéma d'animation TY=Typographie, DN=Design Numérique, IL=Illustration, BD=Bande-Dessinée,
IP = Installation-Performance SE=Sérigraphie, GV=Gravure
PE = Peinture
PH = Photographie
DE = Dessin
AN = Arts Numériques
GR = Graphisme
TY = Typographie
DN = Design Numérique
IL = Illustration
BD = Bande-Dessinée
SE = Sérigraphie
GV = Gravure
``` ```
### AP Codes AP programs (by code or name): `NS` Narration Spéculative, `DPM` Design et
Valid AP program codes: Politique du Multiple, `APS` Atelier Pratiques Situées, `LIENS`, `PACS`.
- `DPM`
- `LIENS`
- `APS`
(These codes must match exactly what exists in the `ap_programs` table) ---
### Language Values ## Import behaviour
Languages should be provided with capital first letter:
- `Français`
- `English`
- `Nederlands`
- etc.
### Format Values - **Transaction per row** — a failed row is rolled back and logged; import
Common format values (case-insensitive, will be normalized): continues.
- `PDF` - **Required:** `Titre` and `Année` (year may be derived from the identifier).
- `Video` - Empty rows (no title and no identifier) are skipped.
- `Audio` - Authors, supervisors, languages and tags are auto-created if missing.
- `Installation` - **Tag normalisation:** `strtolower`, collapse multiple spaces, de-duplicate,
- `Web` cap at 10.
- etc. - Duplicate `Identifiant` → row skipped (not re-inserted).
- Results summary reports: imported count, skipped count, and per-row
`✓`/`✗` messages.
## Import Behavior ---
### Row Processing ## Restore procedure
1. Empty rows (no title and no identifier) are skipped
2. Each row is processed in a transaction
3. If a row fails, it is skipped and logged, but processing continues
### Data Validation Full restore combines the three exports (CSV + DB + files). See
- If title or year is missing, the row is rejected [export.md](export.md) for the step-by-step restore of the database and files;
- Invalid orientation codes result in no orientation being set (null) the CSV is re-importable as described above when rebuilding from scratch.
- Invalid AP codes result in no AP program being set (null)
- Keywords are limited to first 10 if more are provided
### Data Normalization
- All string fields are trimmed of whitespace
- Language and format values are normalized (first letter capitalized, rest lowercase)
- Empty strings are converted to NULL in the database
### Entity Creation
- Authors, supervisors, and keywords are automatically created if they don't exist
- Existing authors are matched by name
- Contact email is only associated with the first author
## Example CSV Structure
```csv
Identifiant,Titre,Sous-titre,Auteur·ice(s),Contact,Promoteur·ice(s),Format,Année,AP,Orientation,Finalité,Mots-clés,Synopsis,Contexte,Remarques,Langue,Autorisation,License,taille,Points sur 20,lien BAIU
TFE-2024-001,Mon projet artistique,Exploration du numérique,"Alice Dupont, Bob Martin",alice@example.com,Prof. Smith,PDF,2024,DPM,AN,Création,art numérique,digital art,interactive installation,Un projet explorant l'intersection de l'art et de la technologie,Réalisé dans le cadre du master,Très bon projet,Français,Public,CC-BY,250MB,16.5,https://baiu.example.org/12345
TFE-2024-002,Design graphique moderne,,Charlie Brown,charlie@example.com,"Prof. A, Prof. B","PDF, Print",2024,LIENS,GR,Design,typographie,graphisme,design,Une exploration de la typographie contemporaine,,,English,Restricted,All rights reserved,50MB,15,
```
## Troubleshooting
### Common Issues
1. **Encoding problems**: Ensure file is saved as UTF-8
2. **Missing columns**: All 21 columns must be present, even if empty
3. **Line breaks in fields**: Ensure fields containing newlines are properly quoted
4. **Quote escaping**: Use double quotes (`""`) to escape quotes within fields
### Import Results
After import, the system will display:
- Number of theses successfully imported
- Number of rows skipped due to errors
- Detailed line-by-line results with success (✓) or error (✗) indicators
## Notes
- The import process preserves the order of authors, supervisors, and keywords
- The first author gets the contact email if provided
- Duplicate detection is not performed - each import creates new entries
- Failed rows do not stop the import process
- All errors are logged to the server error log
+68 -142
View File
@@ -1,172 +1,98 @@
# Search Feature Documentation # Search & Répertoire — Documentation
## Overview Two public browsing surfaces, both handled by `app/src/Controllers/SearchController.php`
The search feature allows users to search across theses using multiple criteria including full-text search and advanced filters. and routed by `app/src/Dispatcher.php`:
## Files Created/Modified | Route | Handler | Purpose |
|-------|---------|---------|
| `/search`, `/search.php` | `handleSearch()` | Full-text query + classic single filters |
| `/repertoire`, `/repertoire.php` | `handleRepertoire()` | Browseable directory with multi-select filters |
### New Files Both only ever expose **published** theses (`is_published = 1`).
1. **search.php** - Main search interface page
2. **create_test_db.php** - Script to generate test database with sample data
3. **search.md** - This documentation file
### Modified Files ---
1. **Database.php** - Added search methods:
- `searchTheses()` - Search with multiple filters
- `countSearchResults()` - Count matching results
- `getAvailableYears()` - Get all years from published theses
- `getOrientations()` - Get all orientations
- `getApPrograms()` - Get all AP programs
- `getFinalityTypes()` - Get all finality types
- `getUsedKeywords()` - Get keywords used in published theses
- `getFormatTypes()` - Get all format types
- `getLanguages()` - Get all languages
2. **inc/header.php** - Added "Rechercher" link to navigation ## HandleSearch (`/search`)
## Searchable Fields `handleSearch()` reads from `$_GET` and renders `app/templates/public/search.php`
with the results fragment (`app/templates/partials/search-results.php`).
The search feature allows filtering by: Searchable text fields (via `Database::searchTheses()`):
- Title, subtitle, synopsis, author names, supervisor names, tags/keywords.
1. **Full-text query** - Searches across: Single-value filters (`collectSearchParams()`):
- Title - `query` — free text
- Subtitle - `year` — exact year
- Synopsis - `orientation` — artistic orientation
- Author names - `ap_program` — AP program
- Supervisor names - `finality` — finality type
- Keywords - `format` — format
- `keyword` — tag/keyword
2. **Year** - Filter by specific year Results are paginated (`limit = 20` default); the **search bar**
(`app/templates/partials/search-bar.php`) submits a GET form to `/search`.
3. **Orientation** - Filter by artistic orientation: ---
- Arts Numériques, Dessin, Cinéma d'animation, Installation-Performance
- Peinture, Photographie, Sculpture, Vidéographie
- Graphisme, Typographie, Design Numérique, Illustration
- Bande-Dessinée, Sérigraphie, Gravure
4. **AP Program** - Filter by atelier pratique: ## HandleRepertoire (`/repertoire`)
- Narration Spéculative
- Design et Politique du Multiple (DPM)
- Atelier Pratiques Situées (APS)
- Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes (LIENS)
5. **Finality** - Filter by master finality: `handleRepertoire()` reads multi-select filter arrays from `$_GET` and renders
- Approfondi `app/templates/public/repertoire.php`, which uses the shared results partial
- Enseignement and `app/templates/partials/repertoire-index.php`.
- Spécialisé
6. **Format** - Filter by work format: Multi-select filters (`collectFilterParams()`, each an array, `_GET` keys):
- Site web, Audio, Vidéo, Performance - `fy[]` — years (validated to 1900–2100)
- Objet éditorial, Installation, Autre - `ap[]` — AP program names
- `or[]` — orientations
- `fi[]` — finalities
- `kw[]` — keywords/tags
7. **Language** - Filter by language (Français, Anglais) Each value is trimmed, length-capped (≤ 100), de-duplicated, and passed through
as sanitised strings — no direct user input reaches SQL.
8. **Keyword** - Filter by specific keyword There is also an HTMX **student preview** popover at
`/repertoire/student-preview` (`handleStudentPreview()` → `student-preview.php`).
9. **Type** - Filter by thesis type: ---
- TFE (final thesis projects)
- Doctoral theses
## Testing the Search Feature ## Rate limiting
### 1. Create Test Database Search is rate-limited via `app/src/RateLimit.php`. See the nginx config
Run the script to generate sample data: (`nginx/xamxam.conf`) for the matching server-side limits.
```bash
cd /home/padlock/dev/posterg-website/front-backend
php create_test_db.php
```
This will create `test.db` in the `formulaire/` directory with: ---
- 6 sample theses (various years, orientations, and programs)
- 5 sample authors
- 3 sample supervisors
- 20 keywords
- Complete relationships (authors, supervisors, keywords, formats, languages)
### 2. Access the Search Page ## Database access
Navigate to: `search.php`
### 3. Test Search Scenarios - **Full-text + single filters:** `Database::searchTheses(array $params, $limit, $offset)`
and `Database::countSearchResults(array $params)`.
- **Keyword/tag autocomplete:** `Database::searchTags(string $query)`.
- **Supervisor autocomplete:** `Database::searchSupervisors($query, $role)`.
- **Language autocomplete:** `Database::searchLanguages(string $query)`.
#### Scenario 1: Full-text Search Queries operate on `v_theses_public`; keyword matching joins the `thesis_tags` /
- Enter "urbain" in the search field `tags` tables (keywords are stored as lowercase-normalised **tags**, not a
- Should find: "Espaces Urbains et Narration Collective" `keywords`/`thesis_keywords` set — see [database.md](database.md)).
#### Scenario 2: Filter by Year All queries use PDO prepared statements and escape `%`/`_` for `LIKE`
- Select year: 2024 (`Database::escapeLikeString`) to prevent wildcard injection.
- Should find: 3 theses from 2024
#### Scenario 3: Filter by Orientation ---
- Select orientation: "Installation-Performance"
- Should find: 2 theses
#### Scenario 4: Filter by AP Program ## Performance notes
- Select AP: "Narration Spéculative"
- Should find: 2 theses
#### Scenario 5: Combined Filters - Critical text/filter columns and the junction tables are indexed
- Enter "performance" in search field (`idx_theses_pub_year`, `idx_theses_*`, `idx_thesis_tags_*`, …).
- Select year: 2024 - `v_theses_public` pre-computes the joins for the common read path.
- Should find: 1 thesis ("Corps et Technologies") - The repertoire filters operate on indexed lookup columns.
#### Scenario 6: Keyword Search ---
- Select keyword: "écologie"
- Should find: "Écologies Affectives"
## Database Schema Reference ## Future enhancements (not yet implemented)
The search uses the `v_theses_public` view which combines: The historical `search.md` listed potential automplete/faceted-search/export
- Main thesis data from `theses` table ideas. Status:
- Related authors via `thesis_authors` junction table
- Related supervisors via `thesis_supervisors` junction table
- Related keywords via `thesis_keywords` junction table
- Related formats via `thesis_formats` junction table
- Related languages via `thesis_languages` junction table
- Predefined values from lookup tables (orientations, ap_programs, finality_types, etc.)
## Features - Auto-complete for tags exists at the form level (`searchTags`)
- Faceted counts, saved searches, result export, and advanced boolean operators
### Pagination are **not** implemented
- Results are paginated (20 items per page)
- Previous/Next navigation
- Numbered page links
### Result Display
- Shows total number of results
- Card-based layout matching the main index page
- Displays: title, author, year, synopsis excerpt
- Links to full thesis detail page
### User Experience
- All filters are optional
- Filters can be combined
- "Réinitialiser" button to clear all filters
- Maintains filter state during pagination
## Security Considerations
- All user inputs are sanitized using `htmlspecialchars()`
- SQL queries use prepared statements with parameter binding
- No direct SQL injection risk
- Only published theses are searchable (`is_published = 1`)
## Future Enhancements
Potential improvements:
1. **Auto-complete** - Suggest keywords/authors as user types
2. **Faceted search** - Show filter counts (e.g., "Peinture (12)")
3. **Sort options** - Sort by year, title, relevance
4. **Save searches** - Allow users to bookmark search queries
5. **Export results** - Export search results as CSV/JSON
6. **Advanced boolean search** - Support AND/OR/NOT operators
7. **Search highlights** - Highlight matching terms in results
8. **Related theses** - Show similar works based on keywords
9. **Statistics** - Show search analytics and popular queries
10. **AJAX search** - Live search without page reload
## Technical Notes
- Uses SQLite LIKE operator for text matching (case-insensitive)
- Searches across GROUP_CONCAT fields in the view for many-to-many relationships
- Efficient use of indexes defined in schema.sql
- Compatible with existing Database.php singleton pattern
+71 -58
View File
@@ -1,82 +1,95 @@
# Security # Security
Vulnerability analysis and resolution status for posterg-website. Current security posture for XAMXAM.
> Based on security audit (2026-02-08). All items tracked below. > This supersedes the earlier `security.md` (2026-02-08 audit). The original
> 16-item audit is closed; the items below reflect the current state.
--- ---
## Resolved ## Authentication — admin
### Infrastructure / Deployment - **PHP session auth:** `app/src/AdminAuth.php`. Password-only (no username).
Credentials previously in a gitignored PHP file; the current build stores the
bcrypt hash in `site_settings.admin_password_hash` (manageable from
`/admin/account`).
- `AdminAuth::requireLogin()` guards every admin action/route.
- Session cookies hardened: `HttpOnly`, `SameSite=Strict`, `Secure`,
`Path=/admin`; regenerated on login.
- nginx `auth_basic` layer has been removed; the PHP session layer is the only
gate. (LDAP-based login is a proposed future enhancement — see
`LDAP_AUTH_PLAN.md` / `LDAP_SPEC.md`. It is **not** implemented.)
| # | Issue | Severity | Resolution | ## Transport & headers
|---|-------|----------|------------|
| 1 | No HTTPS — admin credentials exposed in transit | 🔴 CRITICAL | TLS terminated upstream by reverse proxy. nginx.conf doesn't need to handle TLS directly. |
| 3 | Uploaded files stored inside webroot | 🟠 HIGH | Storage moved to `STORAGE_ROOT` (`/var/www/posterg/storage/`), defined in `config/bootstrap.php`. |
| 4 | File path mismatch — media broken & insecure | 🟠 HIGH | DB paths now storage-relative. New `public/media.php` serves files safely. `memoire.php` and `search.php` use `/media.php?path=…`. Cover recording fixed. |
| 5 | Rate limiter bypassed by IP spoofing (`X-Forwarded-For`) | 🟠 HIGH | `src/RateLimit.php` `getClientIdentifier()` uses `REMOTE_ADDR` only. |
| 6 | `.htaccess` rules silently ignored by nginx | 🟠 HIGH | All rules ported to `nginx/posterg.conf`. See `nginx/HTACCESS_TO_NGINX.md`. |
| 13 | Deprecated `X-XSS-Protection` header | 🔵 LOW | Removed from `nginx/posterg.conf`. |
### Frontend / Assets Enforced in `nginx/xamxam.conf` (see `nginx/docs/SECURITY_HEADERS.md`):
| # | Issue | Severity | Resolution | - **HSTS** (`Strict-Transport-Security`, 730 days, preload)
|---|-------|----------|------------| - **CSP** — `default-src 'self'; … frame-ancestors 'none'` on public pages;
| 10 | CDN stylesheet without SRI | 🟡 MEDIUM | CDN will not be used in production. Self-hosted, eliminating supply-chain risk. | `frame-ancestors 'self'` where the app embeds allowed content; admin CSP
includes `script-src 'unsafe-inline'` for the OverType editor. `object-src 'none'`.
- `X-Frame-Options: DENY` (clickjacking)
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy` (geo/mic/camera disabled)
- `Cross-Origin-Opener-Policy` / `Cross-Origin-Resource-Policy: same-origin`
- `X-Robots-Tag: noindex, nofollow` on `/admin/`
### Code Quality / Defence in Depth `X-XSS-Protection` is intentionally **omitted** (deprecated; see
`nginx/docs/SECURITY_HEADERS.md` for rationale).
| # | Issue | Severity | Resolution | ## Rate limiting
|---|-------|----------|------------|
| 14 | Missing `rel="noreferrer"` on external links | 🔵 LOW | `rel="noopener noreferrer"` applied in `public/admin/thanks.php`. |
| 15 | Unescaped integer outputs | 🔵 LOW | Explicit `(int)` casts added in `public/index.php` and `public/search.php`. |
| 16 | Redundant `DATABASE_PATH` constant | 🔵 LOW | Removed from `config/bootstrap.php`. |
### Admin Panel — Authentication & Sessions Defined in the nginx config `limit_req_zone` and enforced by
`app/src/RateLimit.php`:
| # | Issue | Severity | Resolution | - General requests: `30 r/m`
|---|-------|----------|------------| - Search endpoint: `30 r/m`
| 2 | No PHP-level authentication in admin | 🔴 CRITICAL | `src/AdminAuth.php` implements session guard with `password_verify` + `session_regenerate_id`. All admin files call `AdminAuth::requireLogin()`. Credentials in gitignored `config/admin_credentials.php`. No-op when constant absent (dev/cli-server). | - Admin panel: `300 r/m` (burst 30)
| 8 | Session cookies not hardened | 🟡 MEDIUM | Resolved with #2. `AdminAuth::startSession()` sets `HttpOnly=true`, `SameSite=Strict`, `Secure=true` (off on cli-server), `Path=/admin`, `Lifetime=0`. |
--- The PHP limiter uses `REMOTE_ADDR` only (not `X-Forwarded-For`) to avoid IP
spoofing.
## In Progress ## Files & storage
| # | Issue | Severity | Status | - Uploads live **outside the webroot** under `app/storage/` (`tfe/`, `theses/`),
|---|-------|----------|--------| served on demand via `MediaController`/`FileAccessController` through
| 7 | LIKE wildcard injection in admin search | 🟡 MEDIUM | Public `Database::searchTheses()` escapes `%` and `_` correctly. Same pattern must be applied to admin search and any other raw LIKE queries. | controlled endpoints (`/media`), not direct static access.
- nginx blocks `/storage`, `/src`, `/templates`, DB/sql/env/md files, and hidden
files. The DocumentRoot is `app/public/` only.
- Restricted-file downloads are gated by a request/approval/token flow
(`file_access_*` tables).
- Logs write to `app/storage/logs/` — outside the webroot, not publicly served.
--- ## Injection & output
## Not Yet Implemented - **SQL:** all queries use PDO prepared statements.
- **LIKE wildcards:** `Database::escapeLikeString()` escapes `%` and `_`
(public search and related queries).
- **XSS:** `htmlspecialchars()` on all user-controlled output; integer/ID
inputs cast.
- **CSRF:** per-session tokens (`bin2hex(random_bytes(32))`), compared with
`hash_equals()`.
- **File uploads:** MIME validated (`finfo`); FilePond prevalidation +
server-side checks in `FilepondHandler`.
- **Markdown/HTML:** user content (pages, help blocks) rendered via
`league/commonmark`; HTML in admin-editable content is expected.
| # | Issue | Severity | Files | ## Defence in depth / misc
|---|-------|----------|-------|
| 11 | Missing Content-Security-Policy on public pages | 🟡 MEDIUM | `nginx/posterg.conf` → add CSP header to main server block |
| 9 | `error.log` in web-accessible path | 🟡 MEDIUM | `public/admin/actions/formulaire.php` → use absolute path outside webroot |
| 12 | CSV import missing server-side MIME validation | 🟡 MEDIUM | `public/admin/import.php` → add `finfo` MIME check |
--- - External links use `rel="noopener noreferrer"`.
- Decryption/`Crypto` failures are logged without leaking secrets; SMTP
password is stored encrypted in `smtp_settings` and rotated via
`just reencrypt-password`.
- Admin operations are recorded in `admin_audit_log` (resource, action,
status, IP, User-Agent).
## Priority Order ## Areas to keep monitored
1. ~~🔴 CRITICAL~~ — All done (items 1–2) - Tightening the public CSP (`frame-ancestors 'none'` vs `'self'` on embed
2. 🟡 **MEDIUM** — Items 7, 9, 11, 12 remaining routes) is an active topic — see `TODO.md`.
3. ~~🔵 LOW~~ — All done (items 13–16) - Sensitive file downloads and their expiry/token handling are worth periodic
review as usage grows.
--- See also: `nginx/docs/SECURITY_HEADERS.md`, `nginx/docs/PHP_AUTH_LAYER.md`,
`nginx/docs/PRODUCTION_DEPLOYMENT.md`.
## Good Practices Already in Place
- ✅ SQL injection: all queries use PDO prepared statements
- ✅ XSS output: `htmlspecialchars()` on all user-controlled output
- ✅ CSRF: tokens with `bin2hex(random_bytes(32))`, validated with `hash_equals()`
- ✅ File upload: MIME type validated with `finfo`
- ✅ Input validation: year, IDs, pagination cast to integers
- ✅ LIKE wildcard escaping in public search (`Database::escapeLikeString`)
---
*Last updated: 2026-02-08*
+2 -2
View File
@@ -1,6 +1,6 @@
# Nginx Configuration - Post-ERG # Nginx Configuration - XAMXAM
This directory contains nginx configuration and documentation for the Post-ERG thesis website. This directory contains nginx configuration and documentation for the XAMXAM TFE website.
## 📁 Files ## 📁 Files
+4 -4
View File
@@ -5,8 +5,8 @@
| Header | Value | Purpose | | Header | Value | Purpose |
|--------|-------|---------| |--------|-------|---------|
| `Strict-Transport-Security` | `max-age=63072000; includeSubDomains; preload;` | HSTS — forces HTTPS | | `Strict-Transport-Security` | `max-age=63072000; includeSubDomains; preload;` | HSTS — forces HTTPS |
| `Content-Security-Policy` | `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; block embedding | | `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; block embedding (public pages) |
| `X-Frame-Options` | `SAMEORIGIN` | Prevent clickjacking | | `X-Frame-Options` | `DENY` | Prevent clickjacking |
| `X-Content-Type-Options` | `nosniff` | Prevent MIME-type sniffing | | `X-Content-Type-Options` | `nosniff` | Prevent MIME-type sniffing |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer leakage | | `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer leakage |
| `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` | Disable unused browser APIs | | `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` | Disable unused browser APIs |
@@ -17,7 +17,7 @@
| Header | Value | Purpose | | Header | Value | Purpose |
|--------|-------|---------| |--------|-------|---------|
| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; allows inline scripts for OverType editor | | `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none';` | Restrict resource origins; allows inline scripts for OverType editor |
| `X-Robots-Tag` | `noindex, nofollow` | Prevent search-engine indexing of admin | | `X-Robots-Tag` | `noindex, nofollow` | Prevent search-engine indexing of admin |
These were previously declared in `public/admin/.htaccess` as Apache These were previously declared in `public/admin/.htaccess` as Apache
@@ -38,5 +38,5 @@ to expose response bodies that would otherwise be blocked. Sending it provides
no protection and may introduce risk. no protection and may introduce risk.
**Correct mitigation:** a proper `Content-Security-Policy` header (now done for **Correct mitigation:** a proper `Content-Security-Policy` header (now done for
`/admin/`; public-page CSP is todo item #11). both `/admin/` and public pages; embed routes use `frame-ancestors 'self'`).