mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
Reorganise docs: move historical files to archive/, merge overlapping docs
- Move 12 historical/superseded docs + 1 session log + 1 PDF + 1 HTML plan to archive/ - Merge 4 VM-crash docs into archive/vm-crash-incident.md - Merge LDAP plan + spec into ldap.md - Merge FilePond race investigation into filepond-crash-analysis.md - Merge SPECS.md client notes into spec-sheet.md appendix - Update README index and security.md cross-reference
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
# Current Issues — XAMXAM (2026-05-10)
|
||||
|
||||
## 1. FK constraint violation on thesis save (create + edit)
|
||||
|
||||
**Symptom:** `⚠ SQLSTATE[23000]: Integrity constraint violation: 19 FOREIGN KEY constraint failed`
|
||||
|
||||
**Triggers:**
|
||||
- Editing an imported CSV thesis, changing access type to Interdit, save
|
||||
- Opening any imported thesis edit form and saving *without changes*
|
||||
- Saving the add form with empty fields — form below "Cadre académique" disappears (PHP dies mid-render)
|
||||
|
||||
**Root cause found so far:** `Database::createThesis()` at line ~1860 was doing `(int)$data['orientation_id']` which converts SQL null → PHP `null` → `(int)null` = `0`. Since no row with ID 0 exists in `orientations`, this triggers FK violation. Fixed in commit `55088c94` by using `$v ? (int)$v : null` pattern.
|
||||
|
||||
**Still happening after fix** — suggests another code path has same issue, or the fix wasn't complete. The `updateThesis` path was already safe (uses `?: null`), but the error persists.
|
||||
|
||||
**Debugging added:**
|
||||
- `[DB:updateThesis]` log line with all FK values before query (commit `55088c94`)
|
||||
- `[ThesisEdit] Step 1-6 OK` step-level logging (commit `8734d964`)
|
||||
- `ErrorHandler::log()` with full trace on catch (commit `03ad73f3`)
|
||||
|
||||
**Dev server output:** No error_log visible in dev mode — PHP built-in server sends errors to stderr which may not be captured.
|
||||
|
||||
**Next steps:**
|
||||
- Enable `display_errors=1` in dev mode so FK errors render in browser
|
||||
- Check if `setThesisFormats`, `setThesisLanguages`, `setThesisTags` paths also have `(int)null` → `0` issues
|
||||
- Check `formulaire.php` action file — does it also use `createThesis`?
|
||||
|
||||
---
|
||||
|
||||
## 2. Dev server debugging output
|
||||
|
||||
**Symptom:** No error output visible in browser when PHP crashes.
|
||||
|
||||
**Current config** (`bootstrap.php`):
|
||||
- Dev mode (cli-server): `display_errors=1`, `error_reporting=E_ALL`
|
||||
- Production: `display_errors=0`, `log_errors=1`
|
||||
|
||||
**But:** the admin action files override this:
|
||||
- `formulaire.php` line 5-7: `ini_set('display_errors', 0); ini_set('log_errors', 1);`
|
||||
- `edit.php` action: no override (uses bootstrap defaults)
|
||||
|
||||
**Action needed:** Don't suppress display_errors in dev mode. Check `php_sapi_name()` before overriding.
|
||||
|
||||
---
|
||||
|
||||
## 3. Console warnings
|
||||
|
||||
```
|
||||
Layout was forced before the page was fully loaded. node.js:416:1
|
||||
[file-upload-queue] XamxamInitFileUploads called (twice)
|
||||
```
|
||||
- `file-upload-queue.js` called twice — the script might be included twice (check `add.php` template + the `form.php` partial)
|
||||
|
||||
---
|
||||
|
||||
## 4. Tags: lowercase + dedup + CSV import
|
||||
|
||||
**Status:** Implemented across all paths:
|
||||
- Frontend JS: `normalizeTag()` with `replace(/\s+/g, ' ')`, lowercase
|
||||
- Server fragment: `preg_replace('/\s+/', ' ', strtolower(...))`
|
||||
- Both controllers: `fn(string $t) => strtolower(trim(preg_replace('/\s+/', ' ', $t)))`
|
||||
- CSV import: same normalization (commit `8734d964`)
|
||||
- Minimum 3 tags enforced (commit `8734d964`)
|
||||
|
||||
## 5. ErrorHandler coverage
|
||||
|
||||
**Status:** Applied to 12 admin action files + 6 public controllers + 2 form controllers + partage entry point (commit `03ad73f3`). 77 unit test assertions.
|
||||
|
||||
## Relevant commits (most recent first)
|
||||
|
||||
```
|
||||
55088c94 Fix FK violation: (int)null → 0 in createThesis
|
||||
27378b42 ErrorHandler tests: 77 assertions
|
||||
4d9296fd ErrorHandler: precise FK field extraction from SQLite
|
||||
03ad73f3 ErrorHandler: shared logging across all actions/controllers
|
||||
6b6c62d1 Error logging: step-by-step transaction tracing
|
||||
8734d964 Mots-clés: collapse spaces, minimum 3 keywords
|
||||
dfe1186b Mots-clés: lowercase, dedup, keyboard nav, absolute dropdown
|
||||
8d04d4ba Mots-clés: lowercase enforcement, deduplication, absolute dropdown
|
||||
7fe53f8c Mots-clés: interactive HTMX tag search
|
||||
dd110cc5 Admin mobile block: fix inline style beating media query
|
||||
```
|
||||
|
||||
## Key files to review
|
||||
|
||||
- `app/src/Database.php` — `createThesis()` line ~1830, `updateThesis()` line ~1751, `setThesisFormats/Languages/Tags`
|
||||
- `app/src/Controllers/ThesisCreateController.php` — `submit()` line ~146, `validateAndSanitise()` line ~312
|
||||
- `app/src/Controllers/ThesisEditController.php` — `save()` line ~158
|
||||
- `app/public/admin/actions/formulaire.php` — calls `$ctrl->submit()` (create path)
|
||||
- `app/public/admin/actions/edit.php` — calls `$ctrl->save()` (edit path)
|
||||
- `app/public/admin/index.php` — CSV import at line ~220
|
||||
@@ -0,0 +1,403 @@
|
||||
# Livraisons par mois — xamxam
|
||||
|
||||
## Janvier 2026 (8 commits)
|
||||
|
||||
### Formulaire & soumission · 5
|
||||
- Schéma SQLite : tables theses, auteurs, formats, langues, tags, fichiers
|
||||
- Système de gestion des thèses avec migrations
|
||||
- Formulaire admin : création + édition TFE
|
||||
- Formulaire étudiant via liens de partage (share links)
|
||||
|
||||
### Structure & architecture · 3
|
||||
- Monorepo : organisation en dossiers (formulaire, templates, src, admin)
|
||||
- Justfile, .gitignore, READMEs
|
||||
- Nginx config + déploiement fonctionnel
|
||||
|
||||
---
|
||||
|
||||
## Février 2026 (29 commits)
|
||||
|
||||
### Répertoire & recherche · 9
|
||||
- Moteur de recherche (par nom, année, orientation, AP, mot-clé)
|
||||
- Cards en grille sur la page d'accueil (3×4 responsive)
|
||||
- Footer avec années horizontales, scrollable
|
||||
- Filtres combinables, header de recherche compact
|
||||
|
||||
### Admin & gestion des données · 7
|
||||
- Export / import CSV des TFE
|
||||
- Index admin : liste, pagination, stats
|
||||
- Navigation admin dynamique
|
||||
- CSS admin : composants réutilisables, styles standardisés
|
||||
|
||||
### Formulaire & soumission · 5
|
||||
- Uploads de fichiers (stockage, affichage)
|
||||
- Structure formulaire : champs obligatoires, validation
|
||||
- Extraction buildSearchConditions partagé admin/public
|
||||
|
||||
### Sécurité · 3
|
||||
- Auth guard PHP pour le panneau admin (CRITICAL)
|
||||
- Correction failles HIGH et LOW (TODO.SECURITY.md)
|
||||
|
||||
### Autre · 5
|
||||
- Renommage memoire.php → tfe.php
|
||||
- CSS fixup, serveur dev avec live-reload
|
||||
- Analyse de dépendances (TODO.md)
|
||||
|
||||
---
|
||||
|
||||
## Mars 2026 (74 commits)
|
||||
|
||||
### Accessibilité & sémantique HTML · 15
|
||||
- Refactor sémantique complet de toutes les pages publiques et admin
|
||||
- ARIA : nav aria-label, search role=search, aria-current, skip links
|
||||
- Status badges non dépendants de la couleur (WCAG 1.4.1)
|
||||
- Autofocus premier champ invalide (WCAG 3.3.1)
|
||||
- Sous-titres WebVTT pour vidéos (WCAG 4.1.2)
|
||||
- Cibles tactiles 44×44px (WCAG 2.5.5)
|
||||
- Open Graph / Twitter Card meta tags, meta description
|
||||
- Head partagé public, favicon
|
||||
|
||||
### Architecture code · 16
|
||||
- ThesisCreateController, ThesisEditController : extraction depuis add.php/edit.php (530 lignes → contrôleurs dédiés)
|
||||
- TfeController, SearchController, HomeController : extraction depuis les pages publiques
|
||||
- SystemController : unifie status + logs en onglet système
|
||||
- App.php : classe fondation + flash messages unifiés
|
||||
- Partials de formulaire réutilisables (jury-fieldset, etc.)
|
||||
- Nettoyage code mort, suppression alias Database
|
||||
|
||||
### Répertoire & recherche · 15
|
||||
- Répertoire en 6 colonnes : années, orientations, AP, mots-clés, noms, compte
|
||||
- Filtres HTMX sans rechargement, URL partageable
|
||||
- Tri matched-first dans les colonnes de filtre
|
||||
- Popover étudiant·e (pré-rendu serveur, lazy load)
|
||||
- Page TFE publique : layout auteur > titre > meta+synopsis 2-col > fichiers > jury
|
||||
- Liens hypertextes vers le répertoire sur les métadonnées
|
||||
|
||||
### Admin & gestion des données · 8
|
||||
- Pagination serveur (25/page)
|
||||
- Tri des colonnes dans l'index
|
||||
- Merge status + logs → system.php avec onglets fetch()
|
||||
- Paramètres admin : maintenance + compte
|
||||
- Dialogue d'import CSV inline
|
||||
|
||||
### Formulaire & soumission · 6
|
||||
- Jury : interne/externe, président·e, promoteurices, lecteurices
|
||||
- Mode étudiant simulé dans l'admin (?mode=student)
|
||||
- Aide contextuelle Markdown dans le formulaire
|
||||
|
||||
### Email / SMTP · 7
|
||||
- SmtpRelay : client SMTP natif (socket), AUTH PLAIN, TLS
|
||||
- Test SMTP, vérification des credentials à la sauvegarde
|
||||
- Champs from_name, notify_email
|
||||
- Email de notification admin après soumission
|
||||
|
||||
### Base de données · 5
|
||||
- Migrations : superviseurs multi-rôles, tags renommés depuis keywords
|
||||
- Vues SQL (v_theses_full, v_smtp_active)
|
||||
- WAL mode pour SQLite
|
||||
- Index composites (is_published, year)
|
||||
- Auto-migration au démarrage du serveur
|
||||
|
||||
### CSS & design · 3
|
||||
- Suppression de tous les assets externes (self-hosting)
|
||||
- Polices : Combined → Ductus (titres), BBBDMSans (corps)
|
||||
- Variables de couleur standardisées, dark mode
|
||||
|
||||
### Ops · 5
|
||||
- Déploiement : scripts unifiés, justfile
|
||||
- Renommage Posterg → XAMXAM (code, nginx, docs)
|
||||
- Rate limiting sur les endpoints partage
|
||||
|
||||
### Autre · 4
|
||||
- Analyse architecture PHP vs Flask, recommandations refactoring
|
||||
- flake.nix pour shell de dev Nix
|
||||
- Instructions DEV.md pour macOS
|
||||
|
||||
---
|
||||
|
||||
## Avril 2026 (207 commits)
|
||||
|
||||
### Répertoire & recherche · 28
|
||||
- Répertoire 6 colonnes : fixed headers, colonnes différenciées, scroll contenu
|
||||
- Alignement baseline, padding minimal, police conforme maquette
|
||||
- AP entre crochets (ex: Design et Politique du Multiple [DPM])
|
||||
- Résultats : cartes sans couverture → gradient placeholder
|
||||
- Popover étudiant·e : lazy via HTMX, Cache-Control
|
||||
- Split search.php / repertoire.php (index seul)
|
||||
- HTMX filter colonnes : contrôle scroll, swap sans perte d'état
|
||||
|
||||
### Admin & gestion des données · 34
|
||||
- Tags admin : search, rename inline, merge bulk, delete
|
||||
- Langues admin : search, rename, merge, delete, dédoublonnage
|
||||
- Sidebar TOC (IntersectionObserver), lazy load HTMX
|
||||
- Bulk actions : merge, delete, export (CSV + fichiers ZIP)
|
||||
- Colonnes triables (Auteur, Accès)
|
||||
- Dialogue <dialog> remplace alert/confirm navigateur
|
||||
- Toasts repositionnés, durée plus longue
|
||||
- Export DB → paramètres, bouton Mots-clés retiré
|
||||
- Checkboxes auto-save admin en HTMX
|
||||
- Import CSV : AP aliases, OUI/NON sanitization
|
||||
- Index : sticky thead, colspan stats, barre de sélection bulk
|
||||
|
||||
### Formulaire & soumission · 45
|
||||
- Partials de champs unifiés : text-field, select-field, checkbox-list, file-field
|
||||
- Jury : promoteurices multiples avec ajout/suppression dynamique
|
||||
- Jury ULB conditionnel (finalité Approfondi)
|
||||
- Annexes : checkbox + fichier conditionnel dans Fichiers
|
||||
- Contact visible / contact interne découplés
|
||||
- Formulaire partage : rate limiting, flash messages, StudentEmail
|
||||
- CC4r → CC2r renommé
|
||||
- Fragments HTMX partagés admin / partage : formats+fichiers, licence, langue-autre
|
||||
- Fragments conditionnels selon formats cochés
|
||||
- Aide contextuelle Markdown via HTMX
|
||||
- Identifiant TFE régénéré automatiquement au changement d'année
|
||||
- Récapitulatif exhaustif admin + étudiant (tous les champs)
|
||||
- Mode étudiant simulé dans l'admin
|
||||
|
||||
### Uploads & fichiers · 25
|
||||
- **FilePond** remplace les queues custom (495 lignes de JS supprimées)
|
||||
- Architecture upload asynchrone : process / revert / load / remove
|
||||
- Pool unique TFE (tous formats : PDF, vidéo, audio)
|
||||
- Barre de progression d'upload
|
||||
- Validation inline des fichiers (MIME, taille) via HTMX
|
||||
- Contraintes : 100 MB PDF, 500 MB autres, bentopdf.com
|
||||
- Bannières mergées dans covers (suppression banner_path)
|
||||
- FilePond dans partage (endpoints, auth session)
|
||||
- Prévisualisation des fichiers existants dans FilePond
|
||||
|
||||
### PeerTube · 8
|
||||
- Intégration API PeerTube : upload vidéo/audio, OAuth2
|
||||
- Feature flag (désactivé par défaut, en attente de quota)
|
||||
- Embed iframe dans tfe.php
|
||||
- Gestion des fichiers vidéo/audio → PeerTube OU upload direct
|
||||
|
||||
### Pages statiques / CMS · 10
|
||||
- À propos, Charte, Licence : Markdown → HTML, édition admin
|
||||
- TOC sticky dans la sidebar
|
||||
- Contenus-edit : remplacement EasyMDE → OverType (333→118KB)
|
||||
- Aide formulaire : blocs d'aide éditables en admin
|
||||
|
||||
### CSS & design · 21
|
||||
- Architecture CSS en couches : reset → colors → typography → base → components
|
||||
- Suppression de toutes les classes BEM → sélecteurs sémantiques
|
||||
- Tokens de design unifiés (couleurs, typographie, espacement)
|
||||
- Échelle fluide utopia pour typographie et espacement
|
||||
- Suppression du dark mode, unification des thèmes
|
||||
- Extraction de tous les styles inline vers CSS
|
||||
- Polices : Ductus (display), BBBDMSans (body), @font-face corrigé
|
||||
- Header plus haut, texte plus grand, text-shadow violet
|
||||
- Cache-busting par filemtime
|
||||
|
||||
### Accessibilité · 10
|
||||
- ARIA complet sur le formulaire : aria-errormessage, aria-invalid, aria-describedby
|
||||
- WCAG 2.5.5 : cibles tactiles 44×44px
|
||||
- Formulaire responsive mobile (colonnes empilées à 600px)
|
||||
- Sémantique HTML admin : dl stats, section cards, th scope, landmarks
|
||||
|
||||
### Sécurité · 9
|
||||
- Obfuscation des emails en entités HTML (EmailObfuscator)
|
||||
- Content-Security-Policy (admin + public), HSTS, SameSite cookies
|
||||
- COOP / CORP headers
|
||||
- Rate limiting, flash messages partage
|
||||
- Chiffrement credentials PeerTube + SMTP (AES-256-GCM)
|
||||
|
||||
### Architecture code · 18
|
||||
- Extraction des contrôleurs (create, edit, search, TFE, system)
|
||||
- Partials de formulaire réutilisables, fragments HTMX partagés
|
||||
- Dispatcher + front controller (index.php)
|
||||
- Configuration externalisée (config/apropos.php → SQLite)
|
||||
- Nettoyage code mort, suppression Parsedown, déduplication
|
||||
- Tests unitaires + intégration (Phase 0–4 PHPUnit)
|
||||
- Linting : PHPStan, PHP-CS-Fixer, Biome
|
||||
|
||||
### Email / SMTP · 5
|
||||
- SmtpRelay : TLS peer verification, envelope injection fix, dot-stuffing
|
||||
- SMTP test button, credentials probe on save (connect+auth+quit)
|
||||
- notify_email, email de notification remanié
|
||||
|
||||
### Ops & déploiement · 15
|
||||
- Déploiement automatisé (justfile) : code, dépendances, migrations
|
||||
- Vérification des permissions post-déploiement
|
||||
- Backups SQLite cron (horaire + quotidien)
|
||||
- Renommage Posterg → XAMXAM complet
|
||||
- Exclusion DB/fichiers/backups/logs du rsync
|
||||
- Maintenance.flag, rate limit cache, .env permissions
|
||||
- Script de scan passif de sécurité (pentest)
|
||||
- Scripts de migration, merge schema.sql
|
||||
|
||||
### Logs · 5
|
||||
- AdminLogger : JSON-lines
|
||||
- Logging structuré pour les soumissions admin/partage
|
||||
- Log viewer dans system.php avec tail
|
||||
- Exclusion des logs du git et du déploiement
|
||||
|
||||
---
|
||||
|
||||
## Mai 2026 (156 commits)
|
||||
|
||||
### FilePond & uploads · 30
|
||||
- Correction crash « can't access property main, n.status is undefined » (triple root cause)
|
||||
- Tailles en bytes (plus de confusion toInt('1GB') = 1)
|
||||
- Fichiers temporaires survivent au rechargement (session-track)
|
||||
- Guard no-JS : filepond_mode désactivé par défaut, fallback serveur
|
||||
- Relink fichiers orphelins : file browser + intégration FilePond
|
||||
- Trash policy : suppression → tmp/_trash avec traçabilité DB
|
||||
- Storage prefix : theses/ → documents/
|
||||
- Nettoyage modal : fichiers orphelins listés, suppression
|
||||
- Recap admin : suppressions figures, fichiers en table
|
||||
- FilePond CSV import (storeAsFile, no async)
|
||||
- Tous les inputs fichiers → FilePond standardisés
|
||||
- Prévisualisation existante dans FilePond en mode édition
|
||||
|
||||
### Formulaire & soumission · 42
|
||||
- Brouillon automatique formulaire partage (HTMX POST/GET session)
|
||||
- Auto-save admin checkboxes avec toasts de feedback
|
||||
- Nettoyage périodique des brouillons orphelins (cron)
|
||||
- Autosave contenus-edit avec debounce, toolbar OverType
|
||||
- Thesis status column (two-phase commit lifecycle)
|
||||
- FormBootstrap : DRY entre add/edit
|
||||
- Extraction partage chrome → form-page.php partagé
|
||||
- Split form.css → form-base.css + form-admin.css
|
||||
- Fragments admin/partage séparés (auth + shared templates)
|
||||
- Licence : Libre→CC2r+licence, Interne→opt-in, Interdit→none
|
||||
- Corrections formulaire : identifiant/année, contact, fichiers optionnels
|
||||
- Author name casing (COLLATE NOCASE, idHint lookup)
|
||||
- Contact visible / interne découplage complet (v2)
|
||||
- is_published reset fix, note contextuelle label
|
||||
- Promoteurice array repopulation, migration 028 fix
|
||||
- Export fichiers ZIP avec LINK.txt (URLs PeerTube)
|
||||
- Jury fieldset : old() avec signature corrigée pour partage
|
||||
|
||||
### Tests & QA · 15
|
||||
- PHPUnit setup : 228 tests, couverture 21,27%
|
||||
- Tests unitaires : Crypto, EmailObfuscator, SystemController, StudentEmail, TfeController
|
||||
- Tests intégration : Database, ShareLink, RateLimit
|
||||
- Tests validation : ThesisCreate, ThesisEdit, AutofocusField
|
||||
- PHPStan + PHP-CS-Fixer + Biome pour le linting
|
||||
- Lint-php recipe unifié
|
||||
- Correction test failures + duplicate detection bug
|
||||
|
||||
### Dépendances & modernisation · 8
|
||||
- Composer.json avec autoloader
|
||||
- Parsedown → league/commonmark
|
||||
- Client HTTP custom → Guzzle
|
||||
- SMTP socket → PHPMailer
|
||||
- Code coverage configuration (phpunit.xml)
|
||||
|
||||
### Logging · 6
|
||||
- **Monolog** : 4 canaux PSR-3 (app, admin, error, audit)
|
||||
- Rotation 30 jours, format JSON préservé
|
||||
- Remplace 4 systèmes de log distincts (AdminLogger, AppLogger, ErrorHandler, Audit)
|
||||
- LOG_LEVEL par variable d'environnement
|
||||
- Log viewer : JSON parsé en one-liners lisibles, fallback tail PHP
|
||||
|
||||
### CSS — refactor architecture · 10
|
||||
- Split en couches nommées (reset, colors, typography, base, components, utilities)
|
||||
- Un fichier par composant (links, focus, forms, tables, dialog, media, buttons, badges, toasts, pagination, header, search)
|
||||
- Compatibilité backward (variables.css, common.css wrappers)
|
||||
- Corrections vs refactoring initial : modern-normalize base, !important search, toast feedback
|
||||
- Unnest header.css, suppression doublons status-badge/toast
|
||||
|
||||
### Search & filtres · 6
|
||||
- Recherche publique : un seul grand input, icône loupe positionnée
|
||||
- Filtres finalité + format, boutons plus compacts, Réinitialiser neutre
|
||||
- Filtres actifs dans l'index admin (htmx triggers input/change)
|
||||
|
||||
### Admin · 12
|
||||
- Contenus : Mots-clés + Langues fieldset, sticky thead, max-height scroll
|
||||
- Export : bulk CSV + fichiers, DB export → Paramètres
|
||||
- Dialogue import : bouton Terminé, succès permanent, padding
|
||||
- Sortable columns Auteur/Accès
|
||||
- Sticky thead sur index, langues, mots-clés tables
|
||||
- Rename Liens étudiant·e → ajout nom + dialogue d'édition
|
||||
- Recapitulatif admin : sections → fieldsets, pas de thumbnails
|
||||
|
||||
### Divers · 27
|
||||
- Déploiement : split deploy-code/deploy-deps/deploy-migrate, lockfile checksum
|
||||
- Migration runner : .php migrations, idempotent (no such column)
|
||||
- Backups SQLite cron, trigger-backup, test-restore
|
||||
- Production fixes : client_max_body_size 256M, bars.svg 404, nginx rate limit 300r/m
|
||||
- Merge SMTP fields → single fieldset "Emails"
|
||||
- CSV import : AP aliases, boolean fix, jury roles
|
||||
- Migration 033 deduplicate format_types, 025 fix lowercase collision
|
||||
- PeerTube : embed audio player height, fix curl_close, fix upload (multipart POST)
|
||||
- Standardisation : .btn, multi-auteur, sticky save/cancel
|
||||
- Avant/après unload dialog fix, bookmark.md draggable
|
||||
- Sécurité : pentest script PEP 723, CSP headers, session cookie params
|
||||
|
||||
---
|
||||
|
||||
## Juin 2026 (82 commits)
|
||||
|
||||
### Icônes SVG · 8
|
||||
- Migration complète <img> → SVG inline via icon() helper PHP
|
||||
- 26 templates modifiés, 12 icônes corrigées (fill→currentColor)
|
||||
- Icône recherche remplacée (Phosphor fill-based)
|
||||
- Nettoyage modal : SVG icons, padding/margin, BBBDMSans
|
||||
|
||||
### Pages statiques & TOC · 7
|
||||
- Refonte À propos / Charte / Licence : layout partagé en grille .page-content
|
||||
- TOC sticky → details/summary responsive (collapsible sur mobile)
|
||||
- Ancres de titres stables (CommonMark HeadingPermalinkExtension)
|
||||
- Styles liens standardisés, fix heading anchor links
|
||||
- Apropos : contacts flexibles, sidebar éditables, grille contacts admin
|
||||
- Content-page.css remplace apropos.css (partagé 3 pages)
|
||||
|
||||
### Répertoire · 14
|
||||
- Accordéon mobile pour les colonnes de filtre (≤640px)
|
||||
- Toggle, chevron, badge compteur de filtres actifs
|
||||
- Tri des colonnes : matched first, puis unmatched, alphabetique
|
||||
- Mots-clés : résultats viables en haut
|
||||
- Suppression de la chip bar mobile
|
||||
- Correction re-init accordéon après swap HTMX (bug DOM détaché)
|
||||
- Scroll colonnes : min-height: 0, overflow-y: auto
|
||||
- Scroll-position memory sur swaps HTMX
|
||||
- Transition opacité fluide sur #repertoire-index
|
||||
- CSS répertoire : colonnes proportionnelles, scrollbars discrètes, fontes maquette
|
||||
- Fix word-break dans les en-têtes de colonnes
|
||||
|
||||
### Formulaire · 22
|
||||
- **Accessibilité** : aria-errormessage, aria-invalid, aria-describedby sur tout le formulaire
|
||||
- Layout mobile responsive (600px) : champs empilés, touch targets 44×44px
|
||||
- Autosave brouillon partage (fragment endpoint POST/GET, session persistence)
|
||||
- Guard no-JS uploads (filepond_mode disabled par défaut, fallback serveur)
|
||||
- Preserve fichiers temporaires FilePond après redirection validation
|
||||
- FormBootstrap extraction
|
||||
- Thesis status column (two-phase commit)
|
||||
- FormAdmin CSS séparé
|
||||
|
||||
### Durée TFE · 4
|
||||
- Colonnes DB (duration_hours, duration_minutes, duration_seconds)
|
||||
- Champs formulaire : h:m:s au lieu de minutes/sec/heures
|
||||
- Affichage public : format XhYm
|
||||
- Migration + contrôleurs + vues
|
||||
|
||||
### TFE — corrections métadonnées · 8
|
||||
- Finalité master affichée sur la page publique
|
||||
- Identifiant régénéré si mismatch année (préfixe 4 chiffres)
|
||||
- Author name casing corrigé (COLLATE NOCASE, idHint lookup)
|
||||
- Suppression vidéos PeerTube lors de la suppression d'un TFE
|
||||
- CC2r + licence affichés sur tfe.php, formatage contact court
|
||||
- Meta+synopsis en grille 2 colonnes, underline retiré, inclusive writing
|
||||
|
||||
### Build & performance · 12
|
||||
- **Pipeline build** : biome (lint/format) + rolldown (JS bundles) + lightningcss (CSS bundles/minify)
|
||||
- Gzip activé dans nginx
|
||||
- ~730 lignes de JS inline → 15 fichiers externes
|
||||
- Dev-watch avec chokidar-cli (reconstruction ~200ms)
|
||||
- dist/ pour les assets minifiés
|
||||
- .gitignore vendor/ path, storage/tmp/filepond/
|
||||
|
||||
### Sécurité · 8
|
||||
- Open redirect fix : rejet des URLs protocol-relative (//evil.com)
|
||||
- CSRF sur retry-email POST, gate partage fragments sur share_active session
|
||||
- Suppression App::verifyCsrf() mort
|
||||
- PHP upload limits → 8GB pour vidéos (8192M)
|
||||
- Biome lint fixes (duplicate CSS, arrow functions)
|
||||
|
||||
### Cleanup & divers · 7
|
||||
- Nettoyage dialog : suppression doublons, hx-confirm sur delete
|
||||
- Correctif logs capturés, debug console.log retiré
|
||||
- Type hint adminOld corrigé (string → string|array)
|
||||
- Index SQLite pour requêtes langues/tags (contenus page)
|
||||
- TODO à jour, tasks sécurité complétées
|
||||
Binary file not shown.
@@ -0,0 +1,134 @@
|
||||
# Post-ERG Setup Guide
|
||||
|
||||
Complete setup guide for development and production deployment.
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.4
|
||||
- SQLite3 (`php8.4-sqlite3`)
|
||||
- nginx (production)
|
||||
|
||||
## Development Setup
|
||||
|
||||
### 1. Initial Setup
|
||||
|
||||
```bash
|
||||
just setup
|
||||
```
|
||||
|
||||
### 2. Start Development Server
|
||||
|
||||
```bash
|
||||
just serve
|
||||
```
|
||||
|
||||
Access at: http://localhost:8000
|
||||
|
||||
### 3. Run Tests
|
||||
|
||||
```bash
|
||||
just test
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### First-Time Server Setup
|
||||
|
||||
```bash
|
||||
ssh posterg
|
||||
sudo mkdir -p /var/www/posterg
|
||||
sudo chown www-data:posterg /var/www/posterg
|
||||
sudo chmod 775 /var/www/posterg
|
||||
exit
|
||||
```
|
||||
|
||||
### Deploy Application
|
||||
|
||||
```bash
|
||||
just deploy
|
||||
just deploy-nginx
|
||||
```
|
||||
|
||||
### Set Admin Password
|
||||
|
||||
```bash
|
||||
just manage-admin-users
|
||||
ssh posterg "sudo bash /tmp/manage-admin-users.sh"
|
||||
```
|
||||
|
||||
### Verify Deployment
|
||||
|
||||
```bash
|
||||
# Test public site
|
||||
curl -I https://posterg.erg.be/
|
||||
|
||||
# Test admin protection
|
||||
curl -I https://posterg.erg.be/admin/
|
||||
|
||||
# Test file protection
|
||||
curl -I https://posterg.erg.be/storage/test.db
|
||||
```
|
||||
|
||||
## Nginx Configuration
|
||||
|
||||
See `nginx/SETUP.md` and `nginx/docs/PRODUCTION_DEPLOYMENT.md` for detailed nginx setup.
|
||||
|
||||
## Admin Panel
|
||||
|
||||
The admin panel is protected by:
|
||||
1. nginx HTTP Basic Authentication (htpasswd)
|
||||
2. PHP session authentication
|
||||
|
||||
Manage users with:
|
||||
```bash
|
||||
just manage-admin-users
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
### Initialize Test Database
|
||||
|
||||
```bash
|
||||
just init-db
|
||||
```
|
||||
|
||||
### Reset Database
|
||||
|
||||
```bash
|
||||
just reset-db
|
||||
```
|
||||
|
||||
### Deploy Test Database to Server
|
||||
|
||||
```bash
|
||||
just deploy-db
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
just logs
|
||||
```
|
||||
|
||||
### Stop Development Server
|
||||
|
||||
```bash
|
||||
just stop
|
||||
```
|
||||
|
||||
### Run Migrations
|
||||
|
||||
```bash
|
||||
just migrate
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Admin panel: HTTP Basic Auth + PHP session
|
||||
- File uploads: Stored outside webroot, served via `media.php`
|
||||
- Rate limiting: 30 req/min general, 10 req/min admin
|
||||
- Security headers: X-Frame-Options, CSP, HSTS, etc.
|
||||
|
||||
See `nginx/docs/SECURITY_HEADERS.md` for details.
|
||||
@@ -0,0 +1,139 @@
|
||||
# SMTP 550 — Recipient Address Rejected (`erg.school`)
|
||||
|
||||
**Date:** 2026-04-30
|
||||
**Symptom:** Access-link emails to `@erg.school` addresses fail with:
|
||||
|
||||
```
|
||||
550 5.1.1 <user@erg.school>: Recipient address rejected: User unknown in virtual mailbox table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What is happening
|
||||
|
||||
The SMTP relay that XAMXAM uses to send outbound email is a Postfix instance
|
||||
that is **also configured as the authoritative mail server for `erg.school`**.
|
||||
|
||||
When XAMXAM sends `RCPT TO:<user@erg.school>`, Postfix looks up the address in
|
||||
its local `virtual_mailbox_maps` table. Because the individual mailbox does not
|
||||
exist in that table, Postfix rejects the message permanently with 550 instead
|
||||
of forwarding it outward.
|
||||
|
||||
This affects **all** outbound email to `@erg.school` sent through this relay,
|
||||
regardless of whether the address is real — Postfix never tries to route the
|
||||
message anywhere else.
|
||||
|
||||
---
|
||||
|
||||
## Why it happens
|
||||
|
||||
Postfix owns a domain in one of two ways:
|
||||
|
||||
| Setting | Effect |
|
||||
|---|---|
|
||||
| `mydestination` | Postfix delivers locally via Unix accounts |
|
||||
| `virtual_mailbox_domains` | Postfix delivers locally via `virtual_mailbox_maps` |
|
||||
|
||||
If `erg.school` (or a wildcard matching it) appears in either of these on the
|
||||
outbound relay, Postfix will **never relay** mail to that domain — it will
|
||||
always attempt local delivery and reject unknown recipients.
|
||||
|
||||
To confirm, run on the relay server:
|
||||
|
||||
```bash
|
||||
postconf mydestination
|
||||
postconf virtual_mailbox_domains
|
||||
postconf relay_domains
|
||||
```
|
||||
|
||||
Check whether `erg.school` appears (directly or via a lookup table).
|
||||
|
||||
---
|
||||
|
||||
## Fix options
|
||||
|
||||
### Option A — Preferred: use a different relay for outbound mail
|
||||
|
||||
Configure XAMXAM to send via an SMTP relay that does **not** host `erg.school`
|
||||
(e.g. a dedicated outbound relay, a transactional mail provider, or the
|
||||
outbound smarthost if one exists).
|
||||
|
||||
Change the SMTP settings in the XAMXAM admin panel (`/admin/parametres.php`)
|
||||
to point to that relay.
|
||||
|
||||
---
|
||||
|
||||
### Option B — Remove `erg.school` from local delivery on the relay
|
||||
|
||||
If the relay should not be the final destination for `erg.school` mail, remove
|
||||
it from the relevant Postfix maps.
|
||||
|
||||
**If it is in `mydestination`:**
|
||||
|
||||
```ini
|
||||
# /etc/postfix/main.cf
|
||||
mydestination = localhost, localhost.localdomain
|
||||
# remove erg.school (and any wildcard covering it)
|
||||
```
|
||||
|
||||
**If it is in `virtual_mailbox_domains`:**
|
||||
|
||||
```ini
|
||||
# /etc/postfix/main.cf
|
||||
virtual_mailbox_domains = ...
|
||||
# remove erg.school from the list (or from the referenced lookup table)
|
||||
```
|
||||
|
||||
After editing `main.cf`:
|
||||
|
||||
```bash
|
||||
postfix check
|
||||
systemctl reload postfix
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option C — Add a `transport_maps` override for the domain
|
||||
|
||||
If `erg.school` must remain in `virtual_mailbox_domains` for inbound delivery
|
||||
but outbound mail from XAMXAM should still be relayed, add a transport override
|
||||
so that mail *to* `erg.school` sent by XAMXAM is forwarded to the real MX
|
||||
rather than delivered locally.
|
||||
|
||||
```ini
|
||||
# /etc/postfix/main.cf
|
||||
transport_maps = hash:/etc/postfix/transport
|
||||
```
|
||||
|
||||
```
|
||||
# /etc/postfix/transport
|
||||
erg.school smtp:[mail.erg.school]:25
|
||||
```
|
||||
|
||||
```bash
|
||||
postmap /etc/postfix/transport
|
||||
systemctl reload postfix
|
||||
```
|
||||
|
||||
> **Note:** This approach is fragile — if XAMXAM is on the same server as the
|
||||
> MX, you risk a delivery loop. Option A or B is cleaner.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After applying the fix, test with XAMXAM's built-in SMTP probe
|
||||
(`/admin/parametres.php` → Test SMTP). Then submit a real access request with
|
||||
an `@erg.school` address and confirm the email arrives.
|
||||
|
||||
You can also test directly from the server:
|
||||
|
||||
```bash
|
||||
swaks --to test.user@erg.school \
|
||||
--from xamxam@erg.be \
|
||||
--server <smtp_host> --port 587 \
|
||||
--tls --auth-user <username> --auth-password <password>
|
||||
```
|
||||
|
||||
A successful relay returns `250 2.0.0 Ok: queued as …`.
|
||||
A 550 response confirms the domain is still being caught locally.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,269 @@
|
||||
# CSS Cleanup - Post-ERG
|
||||
|
||||
Complete CSS rewrite removing Bulma dependency and creating a minimalistic, readable design.
|
||||
|
||||
## 🎯 What Changed
|
||||
|
||||
### Removed
|
||||
- ❌ Bulma CSS framework (~200KB)
|
||||
- ❌ External CDN dependency
|
||||
- ❌ Unused CSS bloat
|
||||
|
||||
### Added
|
||||
- ✅ Custom minimalistic CSS (~9KB)
|
||||
- ✅ Clean, modern design
|
||||
- ✅ Fully responsive layout
|
||||
- ✅ Maintained all functionality
|
||||
|
||||
## 📊 Before vs After
|
||||
|
||||
| Metric | Before | After | Improvement |
|
||||
|--------|--------|-------|-------------|
|
||||
| **CSS Size** | ~200KB | ~9KB | **95% smaller** |
|
||||
| **External Deps** | 1 (Bulma CDN) | 0 | **No external deps** |
|
||||
| **Load Time** | ~500ms | ~50ms | **90% faster** |
|
||||
| **Maintainability** | Hard | Easy | **Full control** |
|
||||
|
||||
## 🎨 Design System
|
||||
|
||||
### Color Palette
|
||||
```css
|
||||
--color-primary: #c104fc /* Purple - main accent */
|
||||
--color-secondary: #4da870 /* Green - secondary accent */
|
||||
--color-text: #333 /* Dark gray - main text */
|
||||
--color-text-light: #666 /* Light gray - secondary text */
|
||||
--color-border: #ddd /* Light border */
|
||||
--color-bg: #fff /* White background */
|
||||
--color-bg-light: #f9f9f9 /* Light gray background */
|
||||
```
|
||||
|
||||
### Typography
|
||||
- **System fonts** for speed and readability
|
||||
- **Combined font** for headings (custom font preserved)
|
||||
- **Base size**: 16px (1rem)
|
||||
- **Line height**: 1.6 for readability
|
||||
|
||||
### Spacing
|
||||
- **Base spacing**: 1rem (16px)
|
||||
- **Large spacing**: 2rem (32px)
|
||||
- **Consistent rhythm** throughout
|
||||
|
||||
## 🧩 Components
|
||||
|
||||
All Bulma classes kept working with custom implementations:
|
||||
|
||||
### Layout
|
||||
- `.section` - Page sections with padding
|
||||
- `.container` - Max-width centered container
|
||||
- `.columns` - CSS Grid responsive layout
|
||||
- `.column` - Grid items with responsive sizing
|
||||
|
||||
### Components
|
||||
- `.navbar` - Sticky header with gradient
|
||||
- `.card` - Content cards with hover effects
|
||||
- `.button` - Action buttons
|
||||
- `.notification` - Alert messages
|
||||
- `.box` - Content containers
|
||||
- `.tag` - Labels and badges
|
||||
|
||||
### Form Elements
|
||||
- `.input` - Text inputs
|
||||
- `.textarea` - Multi-line inputs
|
||||
- `.label` - Form labels
|
||||
- `.field` - Form field containers
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
### Breakpoints
|
||||
- **Desktop**: > 768px (multi-column grid)
|
||||
- **Tablet**: 480-768px (2-column grid)
|
||||
- **Mobile**: < 480px (single column)
|
||||
|
||||
### Features
|
||||
- ✅ Responsive navigation
|
||||
- ✅ Flexible grid layout
|
||||
- ✅ Adaptive card sizes
|
||||
- ✅ Touch-friendly targets
|
||||
- ✅ Readable text sizes
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
### Performance
|
||||
- **No external dependencies** - all CSS self-hosted
|
||||
- **Minimal file size** - only 9KB
|
||||
- **Critical CSS only** - no unused styles
|
||||
- **Fast parsing** - simple selectors
|
||||
|
||||
### Accessibility
|
||||
- **High contrast** text
|
||||
- **Focus states** on interactive elements
|
||||
- **Semantic HTML** preserved
|
||||
- **Keyboard navigation** supported
|
||||
|
||||
### Maintainability
|
||||
- **CSS variables** for easy theming
|
||||
- **Clear sections** and comments
|
||||
- **Consistent naming** conventions
|
||||
- **No preprocessor needed**
|
||||
|
||||
## 🔧 Customization
|
||||
|
||||
### Change Colors
|
||||
Edit CSS variables at the top of `posterg.css`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-primary: #c104fc; /* Your brand color */
|
||||
--color-secondary: #4da870; /* Secondary color */
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
### Change Spacing
|
||||
```css
|
||||
:root {
|
||||
--spacing: 1rem; /* Base spacing */
|
||||
--spacing-lg: 2rem; /* Large spacing */
|
||||
}
|
||||
```
|
||||
|
||||
### Change Layout Width
|
||||
```css
|
||||
:root {
|
||||
--max-width: 1200px; /* Maximum content width */
|
||||
}
|
||||
```
|
||||
|
||||
## 📂 Files Modified
|
||||
|
||||
### Updated
|
||||
- `apps/public/inc/header.php` - Removed Bulma link
|
||||
- `apps/public/assets/posterg.css` - Complete rewrite
|
||||
|
||||
### Preserved
|
||||
- `apps/public/assets/normalize.css` - CSS reset (kept)
|
||||
- `apps/public/assets/fonts/` - Custom fonts (kept)
|
||||
|
||||
## ✅ Testing Checklist
|
||||
|
||||
After deployment, verify:
|
||||
|
||||
- [ ] Homepage loads and looks good
|
||||
- [ ] Card grid is responsive
|
||||
- [ ] Navigation works
|
||||
- [ ] Hover effects work on cards
|
||||
- [ ] Search page works
|
||||
- [ ] Individual thesis pages work
|
||||
- [ ] Forms display correctly (admin)
|
||||
- [ ] Mobile layout works
|
||||
- [ ] Tablet layout works
|
||||
- [ ] Desktop layout works
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
The CSS was deployed automatically with:
|
||||
|
||||
```bash
|
||||
just deploy-public
|
||||
```
|
||||
|
||||
This updates:
|
||||
1. `assets/posterg.css` - New minimalistic CSS
|
||||
2. `inc/header.php` - Removed Bulma dependency
|
||||
|
||||
## 🎨 Visual Changes
|
||||
|
||||
### Navigation
|
||||
- ✅ Kept gradient background
|
||||
- ✅ Sticky positioning
|
||||
- ✅ Hover effects
|
||||
- ✅ Custom font preserved
|
||||
|
||||
### Cards
|
||||
- ✅ Clean borders
|
||||
- ✅ Subtle hover effects
|
||||
- ✅ Responsive grid
|
||||
- ✅ Better spacing
|
||||
|
||||
### Typography
|
||||
- ✅ More readable sizes
|
||||
- ✅ Better line heights
|
||||
- ✅ Consistent hierarchy
|
||||
|
||||
## 🔮 Future Improvements
|
||||
|
||||
### Easy Wins
|
||||
- Add dark mode toggle
|
||||
- Add custom color themes
|
||||
- Add print stylesheet
|
||||
- Add animation transitions
|
||||
|
||||
### Advanced
|
||||
- Lazy load images
|
||||
- Add skeleton loaders
|
||||
- Progressive enhancement
|
||||
- Service worker caching
|
||||
|
||||
## 📊 Browser Support
|
||||
|
||||
Tested and working on:
|
||||
- ✅ Chrome/Edge (modern)
|
||||
- ✅ Firefox (modern)
|
||||
- ✅ Safari (modern)
|
||||
- ✅ Mobile browsers
|
||||
|
||||
Uses modern CSS features:
|
||||
- CSS Grid (2017+)
|
||||
- CSS Variables (2016+)
|
||||
- Flexbox (2015+)
|
||||
|
||||
All with excellent browser support (>95%).
|
||||
|
||||
## 🎓 Technical Details
|
||||
|
||||
### CSS Architecture
|
||||
- **Mobile-first** approach
|
||||
- **CSS Grid** for layout
|
||||
- **Flexbox** for components
|
||||
- **CSS Variables** for theming
|
||||
- **BEM-like** naming (kept Bulma classes)
|
||||
|
||||
### No Build Process
|
||||
- Pure CSS (no SCSS/LESS/PostCSS needed)
|
||||
- No JavaScript required
|
||||
- Direct deployment
|
||||
- Easy to debug
|
||||
|
||||
## 💡 Benefits
|
||||
|
||||
### For Users
|
||||
- ⚡ **Faster load times** - 95% less CSS
|
||||
- 📱 **Better mobile experience** - optimized responsive
|
||||
- 🎯 **Cleaner design** - less visual noise
|
||||
- 🌐 **No CDN dependency** - works offline
|
||||
|
||||
### For Developers
|
||||
- 🔧 **Easy to maintain** - simple, clear CSS
|
||||
- 🎨 **Easy to customize** - CSS variables
|
||||
- 🐛 **Easy to debug** - no framework magic
|
||||
- 📚 **Easy to understand** - well-commented code
|
||||
|
||||
### For Performance
|
||||
- 📉 **95% smaller CSS** - 200KB → 9KB
|
||||
- ⚡ **No external requests** - self-hosted
|
||||
- 🚀 **Faster parsing** - simpler selectors
|
||||
- 💾 **Better caching** - static file
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
The new CSS maintains full compatibility with the existing HTML structure. All Bulma classes still work, but are now implemented with custom, lightweight CSS.
|
||||
|
||||
To revert to Bulma (not recommended):
|
||||
```html
|
||||
<!-- In apps/public/inc/header.php -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||
```
|
||||
|
||||
But the custom CSS is faster, smaller, and fully customizable! 🎉
|
||||
@@ -0,0 +1,154 @@
|
||||
# Migration History
|
||||
|
||||
Consolidated record of structural migrations performed on posterg-website.
|
||||
|
||||
---
|
||||
|
||||
## Migration 1: YAML → SQLite (2026-01)
|
||||
|
||||
### What Changed
|
||||
|
||||
Replaced flat YAML file storage with a normalized SQLite database.
|
||||
|
||||
**Before:** Form data saved as individual YAML files in `data/yaml/`, with file uploads in `data/content/` and `data/cover/`.
|
||||
|
||||
**After:** All thesis data stored in a relational SQLite database with 19 tables (11 core, 5 junction, 3 reference), 2 views, proper normalization (3NF), auto timestamps, cascade deletes.
|
||||
|
||||
### Key Changes
|
||||
|
||||
- `Database.php` — PDO wrapper with transaction management, find-or-create methods, lookup helpers
|
||||
- `index.php` — Dynamically loads form options from DB; added subtitle, synopsis, finality, languages, formats
|
||||
- `formulaire.php` — Complete rewrite: transaction-based processing, prepared statements, random cryptographic filenames
|
||||
- `thanks.php` — Reads from DB using thesis ID, displays data from `v_theses_full` view
|
||||
|
||||
### YAML → Database Mapping
|
||||
|
||||
| YAML Field | Database Location |
|
||||
|------------|-------------------|
|
||||
| `auteurice` | `authors.name` |
|
||||
| `email` | `authors.email` |
|
||||
| `année` | `theses.year` |
|
||||
| `titre` | `theses.title` |
|
||||
| `description` | `theses.synopsis` |
|
||||
| `orientation` | `theses.orientation_id` |
|
||||
| `ap` | `theses.ap_program_id` |
|
||||
| `promoteurice` | `supervisors.name` + `thesis_supervisors` |
|
||||
| `tag` | `keywords.keyword` + `thesis_keywords` |
|
||||
| `files` | `thesis_files` table |
|
||||
|
||||
### Data Migration Path
|
||||
|
||||
For importing existing YAML data:
|
||||
1. Parse YAML files with `Symfony\Yaml\Yaml::parseFile()`
|
||||
2. Insert into DB within transactions
|
||||
3. Verify with `SELECT COUNT(*) FROM theses; SELECT * FROM v_theses_full LIMIT 5;`
|
||||
|
||||
---
|
||||
|
||||
## Migration 2: Repository Restructure (2026-02)
|
||||
|
||||
### What Changed
|
||||
|
||||
Restructured from `apps/public/` + `apps/admin/` + `shared/` layout to idiomatic PHP website layout.
|
||||
|
||||
**Before:**
|
||||
```
|
||||
posterg-website/
|
||||
├── apps/public/ # Public website
|
||||
├── apps/admin/ # Admin panel
|
||||
├── shared/ # Shared PHP libraries
|
||||
└── database/
|
||||
```
|
||||
|
||||
**After (intermediate):**
|
||||
```
|
||||
posterg-website/
|
||||
├── index.php # Public root
|
||||
├── admin/ # Admin panel
|
||||
├── lib/ # Shared libraries (was shared/)
|
||||
├── inc/ # Templates (header/footer)
|
||||
├── assets/ # Static files
|
||||
├── database/
|
||||
└── vendor/
|
||||
```
|
||||
|
||||
### Key Changes
|
||||
|
||||
- Moved `apps/public/*` to root
|
||||
- Moved `apps/admin/` to `admin/`
|
||||
- Renamed `shared/` to `lib/`
|
||||
- Updated all `require` paths
|
||||
- Added php-live-reload to `vendor/`
|
||||
|
||||
---
|
||||
|
||||
## Migration 3: Public Directory Structure (2026-02)
|
||||
|
||||
### What Changed
|
||||
|
||||
Moved web-accessible files into `public/` subdirectory so only `public/` is the DocumentRoot.
|
||||
|
||||
**Before:** All files (including config, DB, source) in DocumentRoot — security relied on nginx deny rules.
|
||||
|
||||
**After:**
|
||||
```
|
||||
posterg-website/
|
||||
├── public/ # DocumentRoot — only this exposed
|
||||
│ ├── index.php
|
||||
│ ├── search.php
|
||||
│ ├── admin/
|
||||
│ └── assets/
|
||||
├── config/ # Private
|
||||
├── includes/ # Private (was inc/)
|
||||
├── src/ # Private (was lib/)
|
||||
├── storage/ # Private (DB + uploads)
|
||||
└── var/ # Private (cache, logs)
|
||||
```
|
||||
|
||||
### Key Changes
|
||||
|
||||
- `config/bootstrap.php` — Central path management with constants (APP_ROOT, PUBLIC_ROOT, etc.)
|
||||
- All public PHP files updated to use bootstrap and relative paths
|
||||
- Dev server: `php -S 127.0.0.1:8000 -t public/`
|
||||
- Deployment: rsync to `/var/www/posterg/` (not `/var/www/html/`)
|
||||
- Nginx DocumentRoot: `/var/www/posterg/public`
|
||||
|
||||
### Security Impact
|
||||
|
||||
| Resource | Before | After |
|
||||
|----------|--------|-------|
|
||||
| Database | Accessible if nginx misconfigured | Physically outside web root |
|
||||
| Config | One deny rule away | Physically private |
|
||||
| Source code | Exposed | Physically private |
|
||||
| Dev server | Exposed everything | Matches production security |
|
||||
|
||||
---
|
||||
|
||||
## Migration 4: Simplification (2026-02)
|
||||
|
||||
### What Changed
|
||||
|
||||
Removed package-oriented complexity from the `public/` migration.
|
||||
|
||||
- Removed `var/` directory (completely unused for this website)
|
||||
- Renamed `resources/views/` → `includes/` (simpler, clearer)
|
||||
- Simplified `config/bootstrap.php` from 66 → 33 lines
|
||||
- Removed 10+ unused constants (VAR_ROOT, CACHE_ROOT, LOGS_ROOT, etc.)
|
||||
- Replaced `view()` helper with standard PHP `include`
|
||||
|
||||
### Result
|
||||
|
||||
Only essential constants remain: `APP_ROOT`, `STORAGE_ROOT`, plus environment detection. Security unchanged (still uses `public/` as DocumentRoot).
|
||||
|
||||
---
|
||||
|
||||
## Rollback Notes
|
||||
|
||||
All migrations are tracked in jj. To view history:
|
||||
|
||||
```bash
|
||||
jj log
|
||||
jj edit <previous-change-id>
|
||||
```
|
||||
|
||||
The YAML-based code was kept as a fallback during the SQLite migration. The old `/var/www/html/` structure was kept as a backup during the directory structure migration.
|
||||
@@ -0,0 +1,105 @@
|
||||
# ORM Assessment — posterg
|
||||
|
||||
> **Date:** 2026-03-28
|
||||
> **Scope:** Full codebase review — `src/Database.php`, all `public/` PHP files, `storage/schema.sql`, migrations, tests.
|
||||
|
||||
---
|
||||
|
||||
## Verdict: No ORM needed
|
||||
|
||||
An ORM would add complexity and dependency weight without solving any real pain points in this project. The recommendation is to **keep raw PDO** and instead make one targeted refactor to the `edit.php` file.
|
||||
|
||||
---
|
||||
|
||||
## What the project actually is
|
||||
|
||||
- **PHP 8.4 / SQLite3**, single-file database (`posterg.db`)
|
||||
- ~1 130-line `Database.php` service class wrapping a single `PDO` instance
|
||||
- ~10 public-facing PHP files + ~15 admin PHP files
|
||||
- One developer, non-commercial, low-to-medium traffic school project
|
||||
- No Composer, no framework, no autoloader — intentionally minimal
|
||||
|
||||
---
|
||||
|
||||
## Where SQL lives today
|
||||
|
||||
| Location | SQL type | How parameterised |
|
||||
|---|---|---|
|
||||
| `src/Database.php` | All major queries (reads, writes, views, search) | Named/positional PDO bindings throughout |
|
||||
| `public/admin/actions/formulaire.php` | INSERT thesis, link authors/jury (create path) | PDO positional params |
|
||||
| `public/admin/actions/publish.php` | Bulk/single `UPDATE is_published` | PDO positional params |
|
||||
| `public/admin/edit.php` | Large `UPDATE theses SET …`, DELETE+INSERT junction tables | Raw PDO via `$pdo = $db->getPDO()` — **partially bypasses the service class** |
|
||||
| `public/admin/import.php` | Multi-row INSERT during CSV import | Raw PDO via `$pdo = $db->getPDO()` |
|
||||
|
||||
The pattern is largely consistent: `Database.php` is the canonical data-access layer. Two admin files (`edit.php`, `import.php`) poke through it via `getPDO()` for operations the service class doesn't expose as methods. That's the main rough edge, not something that calls for an ORM.
|
||||
|
||||
---
|
||||
|
||||
## Arguments for an ORM (examined and rejected)
|
||||
|
||||
### "The schema is relational with many junctions"
|
||||
|
||||
The schema is normalised (14 tables, 6 junction tables, 2 views), but every relationship is **read by the database views** (`v_theses_full`, `v_theses_public`) which already aggregate `GROUP_CONCAT` columns. The PHP code barely touches junction tables directly — it calls `setThesisTags()`, `setThesisJury()`, `setThesisLanguages()` etc., which are already encapsulated service methods.
|
||||
An ORM would re-implement these aggregations less efficiently and lose the view layer's performance benefits.
|
||||
|
||||
### "There is duplicated SQL in edit.php / formulaire.php"
|
||||
|
||||
True, but this is a **missing method on `Database.php`**, not an ORM problem. The `updateThesis()` method simply doesn't exist yet; `edit.php` compensates by calling `$db->getPDO()` and writing the UPDATE inline. The fix is a 30-line method addition, not adopting a 50 MB ORM.
|
||||
|
||||
### "Migrations are hand-written SQL files"
|
||||
|
||||
There are 6 migrations in `storage/migrations/`, all trivial (`ALTER TABLE ADD COLUMN`, `CREATE INDEX`, `INSERT OR IGNORE`). This is an appropriate level of complexity for a SQLite project with one schema target (no multi-tenant, no multi-DB sharding). An ORM's migration runner would add overhead without benefit.
|
||||
|
||||
### "Type safety — PHP arrays aren't typed"
|
||||
|
||||
The codebase already returns typed arrays from PDO with `FETCH_ASSOC` and documents return shapes in docblocks (`@return array{license_id:int|null,…}`). PHP 8.4 property types on a couple of DTOs could improve IDE ergonomics if desired, but that's independent of ORM adoption.
|
||||
|
||||
---
|
||||
|
||||
## Arguments against an ORM (decisive)
|
||||
|
||||
### 1. The search query cannot be replicated by an ORM query builder
|
||||
|
||||
`Database::searchTheses()` builds a WHERE clause over `v_theses_public` with up to 9 optional filters including an `EXISTS` subquery over `thesis_tags → tags`. This is a deliberate, LIKE-escaped, parameterised query that maps onto a database view. Every ORM would either:
|
||||
- Force re-implementation as raw SQL passed to the ORM's `raw()` escape hatch (no benefit), or
|
||||
- Generate a massively suboptimal N+1 or multi-JOIN query that duplicates what the view already does.
|
||||
|
||||
### 2. The schema uses database views as the primary read layer
|
||||
|
||||
`v_theses_full` and `v_theses_public` join 15 tables and aggregate via `GROUP_CONCAT`. ORMs treat views as read-only proxies at best; the GROUP_CONCAT columns (`authors`, `keywords`, `languages`, etc.) aren't mappable to ORM collection properties without a custom hydrator. The view approach was a deliberate performance trade-off — an ORM would fight it.
|
||||
|
||||
### 3. SQLite is a poor fit for most PHP ORMs
|
||||
|
||||
Doctrine ORM and Eloquent have first-class SQLite support but with caveats: no `ALTER TABLE ADD COLUMN` rollback, no partial indexes, different date handling, no stored procedures. The current migration approach (plain `.sql` files applied manually/via script) is actually more reliable for SQLite than an ORM migration runner.
|
||||
|
||||
### 4. The project has zero dependencies by design
|
||||
|
||||
`composer.json` does not exist. There is no autoloader. The bootstrap is `require_once __DIR__ . '/../../src/Database.php'`. Introducing an ORM means either Doctrine (~4 MB, 15+ packages) or Eloquent standalone (~2 MB, plus Capsule bootstrapping). For a school project with one SQLite file, this dependency overhead is not justified.
|
||||
|
||||
### 5. The one "leaky abstraction" has a trivial fix
|
||||
|
||||
`edit.php` and `import.php` call `$db->getPDO()` because `Database.php` is missing two methods:
|
||||
|
||||
- `updateThesis(int $id, array $fields): void`
|
||||
- `importThesisRow(array $data): int`
|
||||
|
||||
Adding these two methods completes the service-class encapsulation. After that, **no file outside `src/Database.php` would need raw SQL**.
|
||||
|
||||
---
|
||||
|
||||
## What should actually be done instead
|
||||
|
||||
| Priority | Action |
|
||||
|---|---|
|
||||
| Low | Add `Database::updateThesis()` method to encapsulate the `UPDATE theses SET …` in `edit.php`. |
|
||||
| Low | Add `Database::importThesisRow()` method to encapsulate the CSV import INSERT chain in `import.php`. |
|
||||
| Optional | Add `Database::relinkThesisAuthors()` to replace the delete+reinsert block in `edit.php` (already done for jury/languages/formats/tags). |
|
||||
| Never | Adopt an ORM. |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The current architecture — a single handwritten PDO service class backed by a SQLite database with view-based reads — is **the right tool for this project's scale and context**. The SQL is well-parameterised, the views handle join complexity efficiently, and the one rough edge (two admin pages calling `getPDO()` directly) is a 60-line refactor, not an architectural crisis.
|
||||
|
||||
An ORM would increase complexity, add a large dependency, and provide no benefit that isn't already delivered by `Database.php`.
|
||||
@@ -0,0 +1,176 @@
|
||||
# Posterg: PHP vs Flask Analysis
|
||||
|
||||
## Current Architecture Summary
|
||||
|
||||
- **Stack**: Vanilla PHP (no framework), SQLite, nginx + php-fpm
|
||||
- **Codebase**: ~9,100 lines across 48 PHP files
|
||||
- **Structure**: File-based routing (`public/` = webroot), shared templates via `include`, singleton `Database` class (1,294 lines), custom auth, rate limiting, media proxy
|
||||
- **Pages**: 8 public pages, 17 admin pages (11 views + 7 action handlers)
|
||||
- **Templating**: Raw PHP includes with variable scoping (`$isAdmin`, `$bodyClass`, `$extraCss`, etc.)
|
||||
- **Database**: SQLite via PDO, WAL mode, 13 tables, 2 views, 6 junction tables
|
||||
|
||||
---
|
||||
|
||||
## Templating
|
||||
|
||||
### Current PHP Pain Points
|
||||
|
||||
1. **No template inheritance.** Every page manually sets variables (`$pageTitle`, `$bodyClass`, `$extraCss`, `$ogTags`, `$isAdmin`) then `include`s `head.php`, `header.php`, and `footer.php` in sequence. The head template uses conditionals to branch between admin/public modes — functional but brittle.
|
||||
|
||||
2. **Variable scoping is implicit.** Templates read variables from the caller's scope. There's no contract — if `$availableYears` isn't set before `footer.php` is included, it silently renders nothing. Flask's Jinja2 would make this explicit via `render_template('page.html', years=years)`.
|
||||
|
||||
3. **No block/slot system.** The admin footer injects `$extraJs` / `$extraJsInline` via loose conventions. In Jinja2, `{% block scripts %}` handles this cleanly with override semantics.
|
||||
|
||||
4. **Repeated boilerplate.** Every page repeats the same 5-line preamble: require bootstrap, require Database, set template vars, include head, include header. A Flask `@app.route` + `render_template` collapses this to ~3 lines.
|
||||
|
||||
5. **HTML mixed with logic.** Files like `search.php` (220 lines) interleave DB queries, input validation, pagination math, OG tag construction, and HTML rendering in a single file. Flask naturally separates route handlers from templates.
|
||||
|
||||
### What Flask/Jinja2 Would Improve
|
||||
|
||||
- **Template inheritance**: One `base.html` with `{% block content %}`, `{% block head_extra %}`, `{% block scripts %}`. Admin extends `admin_base.html` which extends `base.html`.
|
||||
- **Macros**: The card rendering loop, pagination nav, and filter dropdowns are all repeated patterns that become `{% macro card(item) %}`.
|
||||
- **Auto-escaping**: Jinja2 escapes by default. The current code manually calls `htmlspecialchars()` ~150 times across the project. One missed call = XSS.
|
||||
- **Explicit context**: `render_template('tfe.html', thesis=data, files=files)` is self-documenting. The current `$data` / `$thesis` / `$item` naming is inconsistent across pages.
|
||||
|
||||
### What Flask Would NOT Improve
|
||||
|
||||
- The templates themselves would be roughly the same size — HTML is HTML.
|
||||
- The OG tag logic in `head.php` is already centralized; Jinja2 wouldn't simplify the conditional logic, just change its syntax.
|
||||
|
||||
---
|
||||
|
||||
## Routing & Code Organization
|
||||
|
||||
### Current State
|
||||
|
||||
File-based routing via nginx → `public/*.php`. Each file is a standalone entry point:
|
||||
```
|
||||
public/index.php → home page
|
||||
public/search.php → search/repertoire
|
||||
public/tfe.php → thesis detail
|
||||
public/admin/edit.php → edit form (GET)
|
||||
public/admin/actions/edit.php → edit handler (POST)
|
||||
```
|
||||
|
||||
This is simple and transparent — the URL *is* the file path. But it means:
|
||||
- No centralized middleware (auth, CSRF, rate limiting are manually required per-file)
|
||||
- No URL generation (hardcoded `href="/admin/edit.php?id=..."` everywhere)
|
||||
- POST handlers are separate files that redirect back, duplicating auth/CSRF boilerplate
|
||||
|
||||
### Flask Equivalent
|
||||
|
||||
```python
|
||||
@app.route('/admin/edit/<int:id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def admin_edit(id):
|
||||
if request.method == 'POST':
|
||||
...
|
||||
return redirect(url_for('admin_edit', id=id))
|
||||
return render_template('admin/edit.html', thesis=thesis)
|
||||
```
|
||||
|
||||
- `@login_required` replaces 7 identical `AdminAuth::requireLogin()` calls + 7 identical CSRF checks
|
||||
- `url_for()` replaces ~50 hardcoded URL strings
|
||||
- GET/POST in one function eliminates the `actions/` directory pattern
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
### Where PHP Already Wins
|
||||
|
||||
1. **Process-per-request model with OPcache.** PHP-FPM with OPcache compiles PHP to bytecode once, then serves each request from shared memory. There is no framework initialization overhead because *there is no framework*. Each request loads only the files it needs.
|
||||
|
||||
2. **SQLite + WAL mode.** The database is local, on-disk, zero-network-hop. The `PRAGMA` settings (WAL, 8MB cache, synchronous=NORMAL) are well-tuned. This is identical regardless of language.
|
||||
|
||||
3. **Low memory footprint.** Each PHP-FPM worker uses ~10-20MB. A Flask process (gunicorn worker) with SQLAlchemy loaded uses ~30-50MB.
|
||||
|
||||
4. **No ORM overhead.** Raw PDO queries with manual bindings are as fast as it gets for SQLite. Flask would likely introduce SQLAlchemy, adding per-query overhead (object hydration, identity map, unit of work tracking).
|
||||
|
||||
5. **Static file serving by nginx.** CSS/JS/fonts are served directly by nginx, never touching PHP. This is identical with Flask behind nginx.
|
||||
|
||||
### Where Flask Would Be Comparable
|
||||
|
||||
| Aspect | PHP (current) | Flask |
|
||||
|--------|--------------|-------|
|
||||
| Cold start | ~5ms (OPcache hit) | ~50-100ms (Python import) |
|
||||
| Warm request | ~2-5ms | ~3-8ms |
|
||||
| SQLite query | Same PDO overhead | Same sqlite3/aiosqlite |
|
||||
| Template render | PHP native | Jinja2 compiled (comparable) |
|
||||
| Concurrency | php-fpm pool (sync) | gunicorn workers (sync) |
|
||||
|
||||
### Where Flask Would Be Worse
|
||||
|
||||
1. **Python is slower for raw computation.** Not relevant here — the bottleneck is SQLite I/O, not CPU.
|
||||
|
||||
2. **No equivalent to OPcache.** Python caches `.pyc` bytecode files, but Jinja2 templates must be compiled at startup or on first access. PHP OPcache stores compiled opcodes in shared memory — inherently faster for the "compile once, serve many" pattern.
|
||||
|
||||
3. **GIL.** Python's GIL limits true parallelism per process. PHP-FPM workers are independent processes with no shared lock. For a database-bound app this is irrelevant, but under high concurrency PHP-FPM scales more linearly.
|
||||
|
||||
4. **Memory per worker.** Flask + dependencies (Werkzeug, Jinja2, click, itsdangerous, markupsafe, plus any ORM) consumes more baseline memory than a PHP-FPM worker running vanilla PHP.
|
||||
|
||||
### Where Flask Would Be Better (Performance)
|
||||
|
||||
1. **Application-level caching.** Flask can hold objects in memory across requests (e.g., cached orientation lists, available years). PHP re-queries these on every request because it shares nothing between requests by default. However, PHP can use APCu for this — it's just not implemented here.
|
||||
|
||||
2. **Connection pooling.** Flask can maintain a persistent SQLite connection per worker. PHP opens a new PDO connection per request (the singleton is per-request, not per-process). For SQLite this overhead is minimal (~0.1ms), but it exists.
|
||||
|
||||
### Verdict: Performance
|
||||
|
||||
**PHP wins marginally for this specific workload.** The app is a low-traffic academic catalogue with SQLite. The differences are in the single-digit millisecond range and completely irrelevant at the expected scale (likely <100 requests/minute). Neither choice would ever be the bottleneck — the network round-trip to the user dwarfs everything.
|
||||
|
||||
---
|
||||
|
||||
## Developer Experience & Maintainability
|
||||
|
||||
### Where Flask Would Be Clearly Better
|
||||
|
||||
1. **Dependency management.** `pip install flask` + `requirements.txt` (or `pyproject.toml`). Currently there are zero external PHP dependencies — which sounds like a feature until you realize the project vendors `Parsedown.php` (2,000 lines of Markdown parsing) instead of using Composer.
|
||||
|
||||
2. **Form handling.** Flask-WTF provides declarative form classes with validation, CSRF built-in, and type coercion. The current code manually validates ~15 fields per form with inconsistent approaches (`filter_var`, `intval`, `trim`, `sanitize_string`).
|
||||
|
||||
3. **Testing.** Flask has a built-in test client (`app.test_client()`) that can simulate full request/response cycles. The current test suite uses a custom `run-tests.php` harness — functional but non-standard.
|
||||
|
||||
4. **Error handling.** Flask has `@app.errorhandler(404)`, `@app.errorhandler(500)`. The current code uses scattered `die()` calls and inconsistent error responses.
|
||||
|
||||
5. **Session management.** Flask-Login provides remember-me, session expiry, next-URL redirect after login. `AdminAuth.php` reimplements a subset of this in 121 lines.
|
||||
|
||||
### Where PHP Is Adequate or Better for This Project
|
||||
|
||||
1. **Zero build step.** Edit a `.php` file, refresh browser. No `flask run`, no virtual environment, no `pip install`. The `php -S localhost:8000` dev server with live-reload is already configured.
|
||||
|
||||
2. **Deployment simplicity.** rsync files to server, done. No virtualenv, no systemd unit for gunicorn, no WSGI/ASGI configuration. PHP-FPM is already running on the server.
|
||||
|
||||
3. **Hosting availability.** Any shared host runs PHP. Flask requires a VPS or PaaS with Python support. However, this project already uses a dedicated server with nginx, so this is moot.
|
||||
|
||||
4. **Team knowledge.** If the maintainers know PHP, rewriting in Python is a net negative regardless of technical merit.
|
||||
|
||||
---
|
||||
|
||||
## Migration Effort
|
||||
|
||||
Rewriting this project in Flask would require:
|
||||
|
||||
| Component | Effort |
|
||||
|-----------|--------|
|
||||
| Database layer (`Database.php` → SQLAlchemy or raw sqlite3) | 2-3 days |
|
||||
| 8 public routes + templates | 2 days |
|
||||
| 17 admin routes + templates | 3-4 days |
|
||||
| Auth system (Flask-Login) | 0.5 days |
|
||||
| File upload/media serving | 1 day |
|
||||
| Rate limiting (Flask-Limiter) | 0.5 days |
|
||||
| nginx config adaptation | 0.5 days |
|
||||
| Testing | 1-2 days |
|
||||
| **Total** | **~10-14 days** |
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Flask would have been a better starting point** for this project — primarily for templating (Jinja2 inheritance eliminates the fragile variable-scoping pattern), routing (decorators + middleware replace per-file boilerplate), and developer ergonomics (form validation, auto-escaping, test client).
|
||||
|
||||
**Flask would not deliver better performance.** The current vanilla PHP stack is actually *slightly faster* for this workload due to OPcache efficiency and the absence of framework overhead. The difference is immaterial at this scale.
|
||||
|
||||
**A rewrite is not justified.** The project works, the architecture is coherent (if verbose), and the codebase is small enough (~9K lines) to maintain. The practical improvements Flask would bring (cleaner templates, less boilerplate, better form handling) don't outweigh the cost of a full rewrite plus the operational change from PHP-FPM to gunicorn.
|
||||
|
||||
**If starting fresh today:** Flask (or even Litestar/FastAPI with Jinja2) would be the stronger choice for a SQLite-backed catalogue app of this size. The Jinja2 templating alone would save ~20% of the current codebase.
|
||||
+4113
File diff suppressed because one or more lines are too long
@@ -0,0 +1,127 @@
|
||||
# System Setup — PHP Extensions
|
||||
|
||||
## Required extensions (in `composer.json`)
|
||||
|
||||
| Extension | Used for | Enabled? |
|
||||
|-----------|----------|----------|
|
||||
| `pdo` | Database abstraction | ✅ |
|
||||
| `pdo_sqlite` | SQLite driver | ✅ |
|
||||
| `sqlite3` | Direct SQLite (migrations) | ✅ |
|
||||
| `openssl` | AES-256-GCM encryption, TLS, CSRNG | ✅ |
|
||||
| `json` | API responses, config, logging | ✅ |
|
||||
| `ctype` | Character type checks (Composer/vendor) | ✅ (polyfill) |
|
||||
| `filter` | Input validation (`filter_var`) | ✅ |
|
||||
| `hash` | Password hashing, checksums | ✅ |
|
||||
| `mbstring` | Multibyte string handling | ✅ (polyfill) |
|
||||
| `iconv` | Character encoding conversion (vendor) | ⚠️ (polyfill) |
|
||||
| `tokenizer` | PHP-CS-Fixer, PHPStan | ✅ |
|
||||
| `fileinfo` | MIME type detection (FilePond uploads) | ✅ |
|
||||
| `curl` | HTTP requests (PeerTube, external APIs) | ✅ |
|
||||
| `zip` | ZIP export (ExportController), vendor packages | ✅ |
|
||||
| `dom` | HTML/XML parsing (vendor, Parsedown→CommonMark) | ✅ |
|
||||
| `libxml` | XML parsing | ✅ |
|
||||
| `session` | Admin auth, CSRF, flash messages | ✅ |
|
||||
| `zlib` | Compression, vendor packages | ✅ |
|
||||
|
||||
## Recommended extensions (not required, but useful)
|
||||
|
||||
| Extension | Purpose | Add? |
|
||||
|-----------|---------|------|
|
||||
| `gd` | Image resizing/thumbnails (cover images) | ⬜ Future |
|
||||
| `exif` | Image metadata extraction | ⬜ Future |
|
||||
| `sodium` | Modern crypto primitives (alternative to openssl) | ⬜ Optional |
|
||||
| `intl` | Unicode collation, date formatting (locale-aware) | ⬜ Optional |
|
||||
|
||||
## Production server (nginx + PHP-FPM)
|
||||
|
||||
The production server runs PHP 8.4 FPM. The nginx config references:
|
||||
```
|
||||
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
|
||||
```
|
||||
|
||||
### Production extension list
|
||||
|
||||
Same as above, plus:
|
||||
- `php8.4-fpm` (the FPM SAPI itself)
|
||||
|
||||
### Enabling an extension on Arch Linux
|
||||
|
||||
Extensions are compiled as shared objects (`.so` files) and live in
|
||||
`/usr/lib/php/modules/`. To enable one:
|
||||
|
||||
```bash
|
||||
# 1. Verify the .so file exists
|
||||
ls /usr/lib/php/modules/iconv.so
|
||||
|
||||
# 2. Add to /etc/php/php.ini
|
||||
echo "extension=iconv" >> /etc/php/php.ini
|
||||
|
||||
# 3. Verify it loaded
|
||||
php -m | grep iconv
|
||||
|
||||
# 4. Restart PHP-FPM (production)
|
||||
sudo systemctl restart php8.4-fpm
|
||||
```
|
||||
|
||||
### Checking the production server
|
||||
|
||||
```bash
|
||||
# SSH into the server
|
||||
ssh xamxam
|
||||
|
||||
# List loaded extensions
|
||||
php -m
|
||||
|
||||
# List available (compiled but not loaded) extensions
|
||||
ls /usr/lib/php/modules/
|
||||
|
||||
# Check PHP-FPM status
|
||||
sudo systemctl status php8.4-fpm
|
||||
```
|
||||
|
||||
## Required shared objects on current dev machine
|
||||
|
||||
The following `.so` files exist in `/usr/lib/php/modules/` but are **not loaded**:
|
||||
|
||||
| Extension | `.so` present? | Currently loaded? | Needed? |
|
||||
|-----------|---------------|-------------------|---------|
|
||||
| `iconv` | ✅ `iconv.so` | ❌ (polyfill) | For production — enable native to avoid polyfill overhead |
|
||||
| `gd` | ❌ | ❌ | Not yet, but useful for cover image thumbnails |
|
||||
| `exif` | ✅ `exif.so` | ❌ | Not yet |
|
||||
| `intl` | ✅ `intl.so` | ❌ | Not yet |
|
||||
| `sodium` | ❌ | ❌ | Not yet |
|
||||
| `bcmath` | ✅ `bcmath.so` | ❌ | No |
|
||||
| `bz2` | ✅ `bz2.so` | ❌ | No |
|
||||
| `gmp` | ✅ `gmp.so` | ❌ | No |
|
||||
| `ldap` | ✅ `ldap.so` | ❌ | No — future LDAP auth planned |
|
||||
|
||||
## Quick enable for production parity
|
||||
|
||||
To match what composer expects natively (no polyfills needed on server):
|
||||
|
||||
```bash
|
||||
# On the server
|
||||
echo "extension=iconv" | sudo tee -a /etc/php/php.ini
|
||||
echo "extension=mbstring" | sudo tee -a /etc/php/php.ini
|
||||
sudo systemctl restart php8.4-fpm
|
||||
```
|
||||
|
||||
`iconv` and `mbstring` are currently satisfied by Symfony polyfills on the dev
|
||||
machine. Enabling them natively on the server is a free performance improvement
|
||||
and avoids a class of polyfill edge cases.
|
||||
|
||||
## composer.json platform requirements
|
||||
|
||||
```json
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-openssl": "*"
|
||||
}
|
||||
```
|
||||
|
||||
These three are declared explicitly because the application cannot function
|
||||
without them. All other extensions (curl, zip, dom, etc.) are required
|
||||
transitively by vendor packages and will cause a `composer install` failure
|
||||
if missing.
|
||||
@@ -0,0 +1,468 @@
|
||||
# PHP Testing Best Practices
|
||||
|
||||
## Standard PHP Testing Structure
|
||||
|
||||
### Industry Standard: PHPUnit
|
||||
|
||||
The de facto standard for PHP testing is **PHPUnit**. Here's how professional PHP projects handle testing:
|
||||
|
||||
## Proper Directory Structure
|
||||
|
||||
```
|
||||
front-backend/
|
||||
├── src/ # Application code (or keep in root for small projects)
|
||||
│ ├── Database.php
|
||||
│ ├── RateLimit.php
|
||||
│ └── ...
|
||||
├── tests/ # All tests go here
|
||||
│ ├── Unit/ # Unit tests (test individual methods)
|
||||
│ │ ├── DatabaseTest.php
|
||||
│ │ └── RateLimitTest.php
|
||||
│ ├── Integration/ # Integration tests (test multiple components)
|
||||
│ │ └── SearchTest.php
|
||||
│ └── Security/ # Security-specific tests
|
||||
│ └── SecurityTest.php
|
||||
├── public/ # Public-facing files (or web root)
|
||||
│ ├── index.php
|
||||
│ ├── search.php
|
||||
│ └── assets/
|
||||
├── vendor/ # Dependencies (git-ignored, not deployed)
|
||||
├── cache/ # Runtime cache (not deployed)
|
||||
├── composer.json # Dependency management
|
||||
├── phpunit.xml # PHPUnit configuration
|
||||
└── .gitignore # Excludes tests, vendor, cache from git
|
||||
```
|
||||
|
||||
## What We Currently Have (Non-Standard)
|
||||
|
||||
```
|
||||
front-backend/
|
||||
├── test_search.php ❌ Tests in root
|
||||
├── test_security.php ❌ No framework
|
||||
├── test_rate_limit.php ❌ Would deploy to production
|
||||
├── create_test_db.php ❌ Test fixture in root
|
||||
└── Database.php ✓ OK
|
||||
```
|
||||
|
||||
## How Professional Projects Work
|
||||
|
||||
### 1. Composer Configuration
|
||||
|
||||
**composer.json** - Proper setup:
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"php": "^7.4|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"symfony/var-dumper": "^6.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit",
|
||||
"test:coverage": "phpunit --coverage-html coverage"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `require`: Production dependencies
|
||||
- `require-dev`: Development/testing dependencies (not deployed)
|
||||
- `autoload-dev`: Test autoloading (not in production)
|
||||
- `scripts`: Convenient test commands
|
||||
|
||||
### 2. PHPUnit Configuration
|
||||
|
||||
**phpunit.xml** - Test configuration:
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
verbose="true">
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Integration">
|
||||
<directory>tests/Integration</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Security">
|
||||
<directory>tests/Security</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
<exclude>
|
||||
<directory>vendor</directory>
|
||||
<directory>tests</directory>
|
||||
</exclude>
|
||||
</coverage>
|
||||
</phpunit>
|
||||
```
|
||||
|
||||
### 3. Example PHPUnit Test
|
||||
|
||||
**tests/Unit/DatabaseTest.php**:
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Database;
|
||||
|
||||
class DatabaseTest extends TestCase
|
||||
{
|
||||
private $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
public function testGetPublishedTheses()
|
||||
{
|
||||
$results = $this->db->getPublishedTheses(10, 0);
|
||||
|
||||
$this->assertIsArray($results);
|
||||
$this->assertLessThanOrEqual(10, count($results));
|
||||
}
|
||||
|
||||
public function testSearchThesesWithWildcard()
|
||||
{
|
||||
$results = $this->db->searchTheses(['query' => '%'], 10, 0);
|
||||
|
||||
// Should return 0 results (wildcards are escaped)
|
||||
$this->assertCount(0, $results);
|
||||
}
|
||||
|
||||
public function testSearchThesesRejectsLongInput()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Search query too long');
|
||||
|
||||
$longQuery = str_repeat('a', 201);
|
||||
$this->db->searchTheses(['query' => $longQuery]);
|
||||
}
|
||||
|
||||
public function testSearchThesesRejectsInvalidYear()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Invalid year');
|
||||
|
||||
$this->db->searchTheses(['year' => 999999]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Running Tests
|
||||
|
||||
```bash
|
||||
# Install dependencies (including dev dependencies)
|
||||
composer install
|
||||
|
||||
# Run all tests
|
||||
composer test
|
||||
# or
|
||||
./vendor/bin/phpunit
|
||||
|
||||
# Run specific test suite
|
||||
./vendor/bin/phpunit --testsuite Unit
|
||||
|
||||
# Run specific test file
|
||||
./vendor/bin/phpunit tests/Unit/DatabaseTest.php
|
||||
|
||||
# Run with coverage report
|
||||
composer test:coverage
|
||||
```
|
||||
|
||||
### 5. .gitignore Configuration
|
||||
|
||||
**.gitignore**:
|
||||
```
|
||||
# Dependencies
|
||||
/vendor/
|
||||
|
||||
# Test artifacts
|
||||
/coverage/
|
||||
/.phpunit.cache/
|
||||
/phpunit.xml.local
|
||||
|
||||
# Cache
|
||||
/cache/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE
|
||||
/.idea/
|
||||
/.vscode/
|
||||
*.swp
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
error.log
|
||||
```
|
||||
|
||||
**Important:** Tests themselves ARE committed to git, but:
|
||||
- `vendor/` is excluded (regenerated via `composer install`)
|
||||
- Test coverage reports are excluded
|
||||
- Cache is excluded
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### What Gets Deployed
|
||||
|
||||
```bash
|
||||
# Option 1: composer install without dev dependencies
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# This installs ONLY 'require' packages, NOT 'require-dev'
|
||||
# Result: No PHPUnit, no test dependencies
|
||||
```
|
||||
|
||||
**Deployed:**
|
||||
- Application code (`src/` or root PHP files)
|
||||
- Production dependencies (`vendor/` - only `require`)
|
||||
- Public assets (`public/`, `assets/`)
|
||||
|
||||
**NOT Deployed:**
|
||||
- `tests/` directory (excluded via deployment config)
|
||||
- Dev dependencies (PHPUnit, etc.)
|
||||
- `cache/` directory
|
||||
- `.git/` directory
|
||||
|
||||
### Deployment Configurations
|
||||
|
||||
**Option 1: .deployignore** (custom deploy scripts):
|
||||
```
|
||||
/tests/
|
||||
/coverage/
|
||||
/.git/
|
||||
/.github/
|
||||
/cache/
|
||||
phpunit.xml
|
||||
phpunit.xml.dist
|
||||
.env.example
|
||||
README*.md
|
||||
*.md
|
||||
```
|
||||
|
||||
**Option 2: rsync with excludes** (like your justfile):
|
||||
```bash
|
||||
rsync -avz \
|
||||
--exclude 'tests/' \
|
||||
--exclude 'coverage/' \
|
||||
--exclude 'cache/' \
|
||||
--exclude '.git/' \
|
||||
--exclude 'phpunit.xml' \
|
||||
--exclude '*.md' \
|
||||
./ server:/var/www/html/
|
||||
```
|
||||
|
||||
**Option 3: Build artifact** (best for large projects):
|
||||
```bash
|
||||
# Build step
|
||||
composer install --no-dev --optimize-autoloader
|
||||
# Creates clean vendor/ with only production deps
|
||||
|
||||
# Then deploy only necessary files
|
||||
```
|
||||
|
||||
## Continuous Integration (CI/CD)
|
||||
|
||||
Professional projects run tests automatically:
|
||||
|
||||
**GitHub Actions** (.github/workflows/tests.yml):
|
||||
```yaml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress
|
||||
|
||||
- name: Run tests
|
||||
run: composer test
|
||||
|
||||
- name: Check security
|
||||
run: ./vendor/bin/phpunit --testsuite Security
|
||||
```
|
||||
|
||||
## Test Types
|
||||
|
||||
### Unit Tests
|
||||
Test individual methods in isolation:
|
||||
```php
|
||||
public function testEscapeLikeString()
|
||||
{
|
||||
$db = new Database();
|
||||
$reflection = new ReflectionClass($db);
|
||||
$method = $reflection->getMethod('escapeLikeString');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($db, 'test%value_here');
|
||||
$this->assertEquals('test\%value\_here', $result);
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
Test multiple components together:
|
||||
```php
|
||||
public function testSearchWithMultipleFilters()
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
$results = $db->searchTheses([
|
||||
'query' => 'urbain',
|
||||
'year' => 2024,
|
||||
'orientation' => 'Arts Numériques'
|
||||
]);
|
||||
|
||||
$this->assertNotEmpty($results);
|
||||
foreach ($results as $result) {
|
||||
$this->assertEquals(2024, $result['year']);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Security Tests
|
||||
Test security measures:
|
||||
```php
|
||||
public function testSqlInjectionPrevention()
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
|
||||
// These should not cause errors or expose data
|
||||
$malicious = ["' OR 1=1--", "'; DROP TABLE theses;--"];
|
||||
|
||||
foreach ($malicious as $injection) {
|
||||
$results = $db->searchTheses(['query' => $injection]);
|
||||
// Treated as literal strings, returns valid results or empty
|
||||
$this->assertIsArray($results);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comparison: Current vs. Standard
|
||||
|
||||
| Aspect | Current Approach | Standard Approach |
|
||||
|--------|------------------|-------------------|
|
||||
| **Location** | Root directory | `tests/` directory |
|
||||
| **Framework** | Raw PHP scripts | PHPUnit |
|
||||
| **Naming** | `test_*.php` | `*Test.php` |
|
||||
| **Running** | `php test_file.php` | `composer test` |
|
||||
| **CI/CD** | Manual | Automated |
|
||||
| **Production** | Must manually exclude | Auto-excluded |
|
||||
| **Coverage** | None | Built-in reporting |
|
||||
| **Assertions** | Manual echoing | PHPUnit assertions |
|
||||
|
||||
## Migration Path for Your Project
|
||||
|
||||
### Minimal Changes (Keep it Simple)
|
||||
|
||||
If you want to keep the current simple approach but make it safer:
|
||||
|
||||
1. **Move tests to `tests/` directory:**
|
||||
```bash
|
||||
mkdir tests
|
||||
mv test_*.php tests/
|
||||
mv create_test_db.php tests/fixtures/
|
||||
```
|
||||
|
||||
2. **Update justfile to exclude tests:**
|
||||
```just
|
||||
deploy:
|
||||
rsync -vur --progress \
|
||||
--exclude 'tests/' \
|
||||
--exclude 'cache/' \
|
||||
--exclude '*.db' \
|
||||
./front-backend/ server:/var/www/html/
|
||||
```
|
||||
|
||||
3. **Add .gitignore:**
|
||||
```
|
||||
/cache/
|
||||
/vendor/
|
||||
*.log
|
||||
test.db
|
||||
```
|
||||
|
||||
### Recommended Approach (Industry Standard)
|
||||
|
||||
For a more professional setup:
|
||||
|
||||
1. **Install PHPUnit:**
|
||||
```bash
|
||||
composer require --dev phpunit/phpunit
|
||||
```
|
||||
|
||||
2. **Convert tests to PHPUnit** (I can help with this)
|
||||
|
||||
3. **Add phpunit.xml configuration**
|
||||
|
||||
4. **Update deployment to use `composer install --no-dev`**
|
||||
|
||||
## Benefits of Standard Approach
|
||||
|
||||
1. **Automatic Exclusion**: Tests never deployed by accident
|
||||
2. **Better Assertions**: PHPUnit provides rich assertion library
|
||||
3. **Coverage Reports**: See which code is tested
|
||||
4. **CI/CD Integration**: Automated testing on every commit
|
||||
5. **IDE Support**: Better integration with PHPStorm, VSCode
|
||||
6. **Mocking**: Easy to mock dependencies
|
||||
7. **Data Providers**: Test same logic with multiple inputs
|
||||
8. **Professional**: Expected by other developers
|
||||
|
||||
## Quick Decision Guide
|
||||
|
||||
**Keep Simple Approach If:**
|
||||
- ✓ Small project (< 10 files)
|
||||
- ✓ Solo developer
|
||||
- ✓ No CI/CD pipeline
|
||||
- ✓ You manually test before deploy
|
||||
|
||||
**Use PHPUnit If:**
|
||||
- ✓ Team project
|
||||
- ✓ Growing codebase
|
||||
- ✓ Want automated testing
|
||||
- ✓ Need coverage reports
|
||||
- ✓ Planning CI/CD
|
||||
|
||||
## Recommendation for Your Project
|
||||
|
||||
Given your project size, I'd suggest a **hybrid approach**:
|
||||
|
||||
1. **Move tests to `tests/` directory** (immediate)
|
||||
2. **Update deployment to exclude `tests/`** (immediate)
|
||||
3. **Keep simple PHP test scripts for now** (works fine)
|
||||
4. **Migrate to PHPUnit later** (when project grows)
|
||||
|
||||
Would you like me to help with any of these approaches?
|
||||
@@ -0,0 +1,161 @@
|
||||
# VM Crash Root Cause Analysis (posterg.erg.be)
|
||||
|
||||
**Date:** 2026-03-26
|
||||
**Server:** posterg.erg.be
|
||||
**Status:** ✅ ROOT CAUSE IDENTIFIED — **NOT the application's fault**
|
||||
|
||||
> Merged from `VM_Crash_Analysis_FINAL.md`, `VM_Crash_Reports.md`,
|
||||
> `EVIDENCE_SUMMARY.md`, and `IMMEDIATE_FIX.md` (single incident).
|
||||
|
||||
---
|
||||
|
||||
## 🔥 ROOT CAUSE: Serial Console (serial-getty) Crash Loop
|
||||
|
||||
The VM did **not** crash due to the nginx/posterg application. The crash was
|
||||
caused by a **systemd `serial-getty@ttyS0` service crash loop** that ran
|
||||
continuously for ~50 days, eventually exhausting system memory.
|
||||
|
||||
### The smoking gun
|
||||
|
||||
- **1,264,488 serial-getty crashes** recorded in the journal
|
||||
- **Restart counter reached 421,491** by the time of the OOM event
|
||||
- **Crashed every 10 seconds** for the entire uptime
|
||||
- Error: `agetty[PID]: could not get terminal name: -22` / `failed to get terminal attributes: Input/output error`
|
||||
|
||||
### Timeline reconstruction
|
||||
|
||||
| Date | Event | Details |
|
||||
|------|-------|---------|
|
||||
| Jan 13, 2026 | System boot | Clean boot, services started normally |
|
||||
| Jan 13 – Mar 4 | Serial getty crash loop | ~421,491 restarts over 48.7 days (6 restarts/min) |
|
||||
| Mar 4, 10:45 | MariaDB memory pressure | InnoDB reports memory pressure event |
|
||||
| Mar 4, 10:50 | OOM Killer triggered | Systemd invokes OOM killer due to memory exhaustion |
|
||||
| Mar 4, 10:51 | Journal stops | System likely became unresponsive |
|
||||
| Mar 4 – Mar 24 | Unknown state | 20-day gap in logs |
|
||||
| Mar 24, 12:56 | Hard reboot | Technicians forced reboot |
|
||||
| Mar 24, 12:57 | System back online | New boot, clean state |
|
||||
|
||||
### Why this happened
|
||||
|
||||
**QEMU/KVM virtual machine configuration issue.** The error
|
||||
`could not get terminal name: -22` (EINVAL) indicates the VM's serial console
|
||||
(ttyS0) is misconfigured or not properly connected at the hypervisor level.
|
||||
|
||||
Common causes: serial console enabled in VM config but not attached to host,
|
||||
QEMU `-serial` parameter misconfigured, VirtIO console driver issue, or
|
||||
host-side serial device permissions.
|
||||
|
||||
### Resource impact
|
||||
|
||||
Each `agetty` spawn creates a process, opens file descriptors, and logs to the
|
||||
journal (~200 bytes per entry). Over 50 days at 6 crashes/minute:
|
||||
|
||||
- ~421,000 failed process spawns
|
||||
- ~1.2 million journal entries (~240MB journal bloat)
|
||||
- Gradual memory exhaustion → OOM killer
|
||||
|
||||
---
|
||||
|
||||
## 🔍 The application is NOT at fault
|
||||
|
||||
Evidence the posterg application is innocent:
|
||||
|
||||
1. **No PHP-FPM crashes** — clean operation, 11.1–11.2M peak memory
|
||||
2. **No nginx errors before OOM** — the 234KB error log is from *after* the
|
||||
reboot (Mar 26), mostly blocked security-scanner attempts
|
||||
3. **Normal traffic** — only internal IP 192.168.6.11 accessing the site
|
||||
4. **No DB issues before crash** — SQLite working fine
|
||||
|
||||
### Post-reboot issues (unrelated to crash)
|
||||
|
||||
After the Mar 24 reboot there were schema errors (`no such table: tags`,
|
||||
`no such column: ts.role`) caused by code updates (Mar 24 14:49) without a
|
||||
matching migration — **not** the crash cause.
|
||||
|
||||
### Post-reboot security events (Mar 26)
|
||||
|
||||
955 blocked requests from 192.168.6.11 (`.env`, `.git/config`, WordPress/
|
||||
Next.js/Nuxt.js probes) — all properly blocked by nginx (working as designed).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ The fix
|
||||
|
||||
Disable the broken serial console service:
|
||||
|
||||
```bash
|
||||
sudo systemctl stop serial-getty@ttyS0.service
|
||||
sudo systemctl disable serial-getty@ttyS0.service
|
||||
sudo systemctl mask serial-getty@ttyS0.service
|
||||
|
||||
# Verify
|
||||
sudo systemctl status serial-getty@ttyS0.service # → "Loaded: masked"
|
||||
```
|
||||
|
||||
**Also fix the post-reboot DB schema errors:**
|
||||
|
||||
```bash
|
||||
cd /var/www/posterg
|
||||
ls -la storage/migrations/
|
||||
sqlite3 storage/posterg.db "SELECT name FROM sqlite_master WHERE type='table';"
|
||||
```
|
||||
|
||||
### Optional: fix the serial console properly (hypervisor)
|
||||
|
||||
If serial console access is needed for emergency recovery, configure it on the
|
||||
QEMU/KVM host via `virsh edit posterg` (add/verify `<serial type='pty'>` +
|
||||
`<console ...>`), restart the VM in a maintenance window, then unmask/re-enable
|
||||
`serial-getty@ttyS0`.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Post-reboot system health
|
||||
|
||||
✅ All systems healthy — memory 6% used, disk 12% used, swap unused, load idle.
|
||||
nginx 4 workers, PHP-FPM 2 workers, MariaDB 155MB RSS (all normal).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Recommendations
|
||||
|
||||
1. **CRITICAL:** disable `serial-getty@ttyS0` (see fix above)
|
||||
2. **Fix DB schema** for post-reboot errors
|
||||
3. **Improve monitoring** — `prometheus-node-exporter` or systemd unit monitoring
|
||||
would have surfaced the serial-getty loop earlier
|
||||
4. **Journal maintenance:**
|
||||
```bash
|
||||
sudo journalctl --disk-usage
|
||||
sudo journalctl --vacuum-size=500M
|
||||
sudo journalctl --vacuum-time=30d
|
||||
# /etc/systemd/journald.conf: SystemMaxUse=500M, SystemKeepFree=1G, MaxRetentionSec=30day
|
||||
```
|
||||
5. **Optional:** tighten `limit_req` rates and add `fail2ban` for repeated 403s
|
||||
|
||||
---
|
||||
|
||||
## 📎 Appendix: technical details
|
||||
|
||||
### OOM event
|
||||
|
||||
```
|
||||
Mar 04 10:50:23 posterg kernel: systemd invoked oom-killer
|
||||
gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0
|
||||
```
|
||||
|
||||
### Serial getty error code
|
||||
|
||||
`agetty[PID]: could not get terminal name: -22` — EINVAL, terminal
|
||||
initialization on a misconfigured ttyS0 device.
|
||||
|
||||
### Journal statistics
|
||||
|
||||
- Total journal entries: ~193 MB
|
||||
- Serial-getty crashes: 1,264,488 (~65% of journal)
|
||||
- Uptime at OOM: ~50 days (Jan 13 – Mar 4)
|
||||
- Crash frequency: every 10s; total restarts 421,491
|
||||
|
||||
---
|
||||
|
||||
**Report prepared by:** Automated analysis + human review
|
||||
**Confidence:** 🟢 HIGH (definitively identified from kernel/journal/service logs)
|
||||
**Risk:** before fix 🟠 HIGH (will recur ~50 days) · after fix 🟢 LOW
|
||||
Reference in New Issue
Block a user