mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
docs: verify and refactor documentation to match current codebase
This commit is contained in:
@@ -62,3 +62,6 @@ app/public/assets/dist/
|
|||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
|
||||||
|
# td (TODO manager) local database
|
||||||
|
.todo.sqlite
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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).
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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]
|
||||||
|
|||||||
+153
-156
@@ -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
|
||||||
|
|
||||||
1. **Setup server directory** (one time):
|
SQLite backups are taken with a WAL-safe hot backup (`sqlite3 .backup`) then
|
||||||
```bash
|
gzipped to `/var/backups/xamxam/` on the server.
|
||||||
just setup-server
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Deploy application**:
|
| Command | Purpose |
|
||||||
```bash
|
|---------|---------|
|
||||||
just deploy
|
| `just deploy-backup` | Install backup script + cron jobs (one-shot) |
|
||||||
```
|
| `just deploy-backup-script` | Install `/usr/local/bin/backup-sqlite.sh` |
|
||||||
Uploads to `/var/www/posterg/`, excludes tests/docs/vendor.
|
| `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 |
|
||||||
|
|
||||||
3. **Deploy nginx config**:
|
Retention: hourly backups kept 30 days, nightly (02:00) backups kept 90 days.
|
||||||
```bash
|
Backup files: `/var/backups/xamxam/db-<timestamp>.db.gz`.
|
||||||
just deploy-nginx
|
|
||||||
ssh posterg
|
|
||||||
sudo bash /tmp/deploy-production.sh
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Verify**:
|
Draft cleanup is handled by a separate cron (`/etc/cron.d/xamxam-cleanup`),
|
||||||
```bash
|
installed via `just deploy-cleanup-cron`, logging to
|
||||||
just server-status
|
`/var/log/xamxam-cleanup.log`. Verify with `just deploy-check-cleanup-log`.
|
||||||
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
|
---
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
just deploy-migrate-storage # apply
|
||||||
|
just deploy-migrate-storage --dry-run # dry-run only
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Because there is no repo on the remote, rollback means restoring files and/or
|
||||||
|
restoring the database from a backup.
|
||||||
|
|
||||||
|
### Restore the database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh xamxam
|
||||||
|
sudo systemctl stop nginx
|
||||||
|
cp /var/backups/xamxam/db-<timestamp>.db.gz /tmp/restore.db.gz
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore application files
|
||||||
|
|
||||||
|
Re-run `just deploy` from a good local state. In jj you can jump back to a
|
||||||
|
previous commit and redeploy:
|
||||||
|
|
||||||
|
```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
|
sudo systemctl status php8.4-fpm
|
||||||
just setup-server # Fixes directory permissions
|
sudo systemctl restart php8.4-fpm
|
||||||
# Then log out/in on server and retry
|
```
|
||||||
```
|
- **Nginx config error**
|
||||||
|
```bash
|
||||||
Manual fix:
|
sudo nginx -t
|
||||||
```bash
|
```
|
||||||
ssh posterg
|
- **`deploy-db` refuses to run**
|
||||||
sudo chown -R www-data:posterg /var/www/posterg
|
The remote DB already exists. Remove it manually only if you intend to
|
||||||
sudo find /var/www/posterg -type d -exec chmod 2775 {} \;
|
overwrite production data.
|
||||||
sudo find /var/www/posterg -type f -exec chmod 664 {} \;
|
- **Composer did not pick up new classes**
|
||||||
sudo chmod 660 /var/www/posterg/storage/*.db
|
`deploy-deps` runs `composer dump-autoload` when the lock checksum is
|
||||||
```
|
unchanged; if that still fails, force a reinstall.
|
||||||
|
- **Backup/schedule not running**
|
||||||
### Nginx 403 Forbidden
|
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`.
|
||||||
```bash
|
|
||||||
ssh 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
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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'`).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user