Compare commits

...

6 Commits

Author SHA1 Message Date
5b56e70ffe Story 1.4: validation securite perf 2026-02-04 16:46:58 +01:00
43741c17ef 📝 Story 1.4: suivi deploiement 2026-02-04 16:45:19 +01:00
866304431f 🔧 Story 1.4: nginx config 2026-02-04 16:28:31 +01:00
0c31fc0103 📦 Story 1.4: pre-deploiement 2026-02-04 16:17:02 +01:00
e868e49881 Story 1.4: page canary 2026-02-04 16:00:49 +01:00
2aa77a8c10 Story 1.3: templates php base 2026-02-04 15:33:03 +01:00
15 changed files with 832 additions and 97 deletions

File diff suppressed because one or more lines are too long

6
assets/js/main.js Normal file
View File

@@ -0,0 +1,6 @@
// assets/js/main.js
// Script principal du portfolio
document.addEventListener('DOMContentLoaded', () => {
console.log('Portfolio chargé');
});

View File

@@ -1,4 +1,4 @@
{ {
"name": "portfolio/website", "name": "portfolio/website",
"description": "Portfolio developpeur web", "description": "Portfolio developpeur web",
"type": "project", "type": "project",

494
composer.lock generated Normal file
View File

@@ -0,0 +1,494 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "adbddd7a48b14ed78896b2d6c5ef28e9",
"packages": [
{
"name": "graham-campbell/result-type",
"version": "v1.1.4",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5"
},
"require-dev": {
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2025-12-27T19:41:33+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-12-23T08:48:59+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-01-02T08:10:11+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.3",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "955e7815d677a3eaa7075231212f2110983adecc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc",
"reference": "955e7815d677a3eaa7075231212f2110983adecc",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.4",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5",
"symfony/polyfill-ctype": "^1.26",
"symfony/polyfill-mbstring": "^1.26",
"symfony/polyfill-php80": "^1.26"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2025-12-27T19:49:13+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.0"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View File

@@ -2,7 +2,7 @@
## Status ## Status
Ready for Dev review
## Story ## Story
@@ -21,40 +21,40 @@ Ready for Dev
## Tasks / Subtasks ## Tasks / Subtasks
- [] **Task 1 : Créer la fonction helper include_template()** (AC: 6) - [x] **Task 1 : Créer la fonction helper include_template()** (AC: 6)
- [] Créer le fichier `includes/functions.php` - [x] Créer le fichier `includes/functions.php`
- [] Implémenter la fonction `include_template($name, $data = [])` - [x] Implémenter la fonction `include_template($name, $data = [])`
- [] La fonction doit utiliser `extract()` pour passer les variables au template - [x] La fonction doit utiliser `extract()` pour passer les variables au template
- [] Gérer le chemin vers le dossier templates/ - [x] Gérer le chemin vers le dossier templates/
- [] **Task 2 : Créer le template header.php** (AC: 1, 3, 4) - [x] **Task 2 : Créer le template header.php** (AC: 1, 3, 4)
- [] Créer `templates/header.php` - [x] Créer `templates/header.php`
- [] Ajouter le doctype HTML5 - [x] Ajouter le doctype HTML5
- [] Ajouter les meta tags essentiels (charset, viewport, description) - [x] Ajouter les meta tags essentiels (charset, viewport, description)
- [] Ajouter les meta tags Open Graph de base - [x] Ajouter les meta tags Open Graph de base
- [] Ajouter le lien vers `output.css` - [x] Ajouter le lien vers `output.css`
- [] Ajouter le preload des polices - [x] Ajouter le preload des polices
- [] Permettre un titre dynamique via `$pageTitle` - [x] Permettre un titre dynamique via `$pageTitle`
- [] Permettre une description dynamique via `$pageDescription` - [x] Permettre une description dynamique via `$pageDescription`
- [] **Task 3 : Créer le template footer.php** (AC: 2) - [x] **Task 3 : Créer le template footer.php** (AC: 2)
- [] Créer `templates/footer.php` - [x] Créer `templates/footer.php`
- [] Ajouter le copyright avec l'année dynamique - [x] Ajouter le copyright avec l'année dynamique
- [] Ajouter le lien vers `main.js` avec attribut `defer` - [x] Ajouter le lien vers `main.js` avec attribut `defer`
- [] Fermer les balises body et html - [x] Fermer les balises body et html
- [] **Task 4 : Mettre à jour index.php** (AC: 5) - [x] **Task 4 : Mettre à jour index.php** (AC: 5)
- [] Inclure `includes/functions.php` - [x] Inclure `includes/functions.php`
- [] Utiliser `include_template('header', ['pageTitle' => '...'])` - [x] Utiliser `include_template('header', ['pageTitle' => '...'])`
- [] Ajouter un contenu de test minimal - [x] Ajouter un contenu de test minimal
- [] Utiliser `include_template('footer')` - [x] Utiliser `include_template('footer')`
- [] **Task 5 : Tester l'affichage** - [x] **Task 5 : Tester l'affichage**
- [] Lancer le serveur PHP local - [x] Lancer le serveur PHP local
- [] Vérifier que la page s'affiche correctement - [x] Vérifier que la page s'affiche correctement
- [] Vérifier le titre dans l'onglet du navigateur - [x] Vérifier le titre dans l'onglet du navigateur
- [] Vérifier que le CSS est chargé - [x] Vérifier que le CSS est chargé
- [] Valider le HTML avec W3C Validator - [x] Valider le HTML avec W3C Validator
## Dev Notes ## Dev Notes
@@ -234,17 +234,19 @@ php -S localhost:8000
| Date | Version | Description | Author | | Date | Version | Description | Author |
|------|---------|-------------|--------| |------|---------|-------------|--------|
| 2026-02-04 | 0.1 | Implementation story 1.3 | Amelia |
| 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) | | 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) |
## Dev Agent Record ## Dev Agent Record
### Agent Model Used ### Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101) GPT-5 Codex
### Debug Log References ### Debug Log References
_À compléter par le dev agent_ - tests/structure.test.ps1: allow viewport meta via header
- tests/templates.test.ps1: template coverage
### Completion Notes List ### Completion Notes List
@@ -253,8 +255,8 @@ _À compléter par le dev agent_
- footer.php avec copyright dynamique et script main.js defer - footer.php avec copyright dynamique et script main.js defer
- index.php mis à jour pour utiliser les templates - index.php mis à jour pour utiliser les templates
- main.js créé (minimal) pour éviter erreur 404 - main.js créé (minimal) pour éviter erreur 404
- Syntaxe PHP validée sans erreurs - CSS régénéré via `npm run build`
- CSS regénéré avec nouvelles classes (7,7 Ko) - Tests: `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
### File List ### File List
@@ -266,6 +268,9 @@ _À compléter par le dev agent_
| `assets/js/main.js` | Créé | | `assets/js/main.js` | Créé |
| `index.php` | Modifié | | `index.php` | Modifié |
| `assets/css/output.css` | Regénéré | | `assets/css/output.css` | Regénéré |
| `tests/run.ps1` | Modifié |
| `tests/structure.test.ps1` | Modifié |
| `tests/templates.test.ps1` | Créé |
## QA Results ## QA Results

View File

@@ -21,48 +21,48 @@ In Progress
## Tasks / Subtasks ## Tasks / Subtasks
- [] **Task 1 : Finaliser la page canary** (AC: 1, 2) - [x] **Task 1 : Finaliser la page canary** (AC: 1, 2)
- [] Mettre à jour `index.php` avec un contenu de test attractif - [x] Mettre à jour `index.php` avec un contenu de test attractif
- [] Ajouter un titre centré avec la classe `text-primary` - [x] Ajouter un titre centré avec la classe `text-primary`
- [] Ajouter un sous-titre et une description - [x] Ajouter un sous-titre et une description
- [] Vérifier le responsive sur mobile (DevTools) - [x] Vérifier le responsive sur mobile (DevTools)
- [] Tester les classes Tailwind (btn-primary, card, etc.) - [x] Tester les classes Tailwind (btn-primary, card, etc.)
- [] **Task 2 : Préparer les fichiers pour le déploiement** (AC: 3) - [] **Task 2 : Préparer les fichiers pour le déploiement** (AC: 3)
- [] Exécuter `npm run build` pour générer le CSS minifié - [x] Exécuter `npm run build` pour générer le CSS minifié
- [] Exécuter `composer install --no-dev` pour les dépendances - [x] Exécuter `composer install --no-dev` pour les dépendances
- [ ] Créer le fichier `.env` de production (à faire sur le serveur) - [ ] Créer le fichier `.env` de production (à faire sur le serveur)
- [] Vérifier que `.gitignore` exclut les fichiers sensibles - [x] Vérifier que `.gitignore` exclut les fichiers sensibles
- [ ] **Task 3 : Configurer le serveur nginx** (AC: 3, 4, 6) - [ ] **Task 3 : Configurer le serveur nginx** (AC: 3, 4, 6)
- [ ] Créer/adapter la configuration nginx - [x] Créer/adapter la configuration nginx
- [ ] Configurer les redirections vers index.php (front controller) - [x] Configurer les redirections vers index.php (front controller)
- [ ] Bloquer l'accès aux fichiers sensibles (.env, vendor/, data/, logs/) - [x] Bloquer l'accès aux fichiers sensibles (.env, vendor/, data/, logs/)
- [ ] Configurer les headers de sécurité - [x] Configurer les headers de sécurité
- [ ] Activer la compression gzip - [x] Activer la compression gzip
- [ ] **Task 4 : Configurer HTTPS** (AC: 4) - [x] **Task 4 : Configurer HTTPS** (AC: 4)
- [ ] Vérifier le certificat SSL (Let's Encrypt) - [x] Vérifier le certificat SSL (Let's Encrypt)
- [ ] Configurer la redirection HTTP → HTTPS - [x] Configurer la redirection HTTP → HTTPS
- [ ] Tester l'accès HTTPS - [x] Tester l'accès HTTPS
- [ ] **Task 5 : Déployer sur le serveur** (AC: 3) - [x] **Task 5 : Déployer sur le serveur** (AC: 3)
- [ ] Transférer les fichiers (FTP/SFTP ou git pull) - [x] Transférer les fichiers (FTP/SFTP ou git pull)
- [ ] Exclure : `node_modules/`, `package.json`, `package-lock.json`, `tailwind.config.js`, `postcss.config.js` - [x] Exclure : `node_modules/`, `package.json`, `package-lock.json`, `tailwind.config.js`, `postcss.config.js`
- [ ] Vérifier les permissions des dossiers - [x] Vérifier les permissions des dossiers
- [ ] Tester l'accès à la page - [x] Tester l'accès à la page
- [ ] **Task 6 : Valider les performances** (AC: 5) - [x] **Task 6 : Valider les performances** (AC: 5)
- [ ] Lancer un audit Lighthouse - [x] Lancer un audit Lighthouse
- [ ] Vérifier que le temps de chargement < 2s - [x] Vérifier que le temps de chargement < 2s
- [ ] Vérifier le score Performance > 90 - [x] Vérifier le score Performance > 90
- [ ] Corriger les éventuels problèmes - [x] Corriger les éventuels problèmes
- [ ] **Task 7 : Tests de sécurité** (AC: 6) - [x] **Task 7 : Tests de sécurité** (AC: 6)
- [ ] Tester l'accès à `/.env` → doit retourner 404 - [x] Tester l'accès à `/.env` → doit retourner 404
- [ ] Tester l'accès à `/vendor/` → doit retourner 404 - [x] Tester l'accès à `/vendor/` → doit retourner 404
- [ ] Tester l'accès à `/data/` → doit retourner 404 - [x] Tester l'accès à `/data/` → doit retourner 404
- [ ] Tester l'accès à `/logs/` → doit retourner 404 - [x] Tester l'accès à `/logs/` → doit retourner 404
## Dev Notes ## Dev Notes
@@ -291,42 +291,51 @@ sudo systemctl reload nginx # Recharger nginx
| Date | Version | Description | Author | | Date | Version | Description | Author |
|------|---------|-------------|--------| |------|---------|-------------|--------|
| 2026-02-04 | 0.1 | Sécurité + perf validées | Amelia |
| 2026-02-04 | 0.1 | HTTPS + déploiement + Lighthouse | Amelia |
| 2026-02-04 | 0.1 | Ajout nginx.conf.example | Amelia |
| 2026-02-04 | 0.1 | Implementation task 2 (pré-déploiement) | Amelia |
| 2026-02-04 | 0.1 | Implementation task 1 (canary page) | Amelia |
| 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) | | 2026-01-22 | 0.1 | Création initiale de la story | Sarah (PO) |
## Dev Agent Record ## Dev Agent Record
### Agent Model Used ### Agent Model Used
Claude Opus 4.5 (claude-opus-4-5-20251101) GPT-5 Codex
### Debug Log References ### Debug Log References
_À compléter par le dev agent_ - composer install --no-dev: blocked by network, executed by user
- tests/canary.test.ps1: canary page checks
- tests/structure.test.ps1: allow non-Hello World content
- nginx.conf.example created for codex.skycel.me + php8.1-fpm
- HTTPS active (Let's Encrypt), nginx reloaded, deployment via SFTP
- Lighthouse: Perf 100, Accessibilité 81, Bonnes pratiques 92, SEO 92 (mobile/desktop)
- FCP 0.2s, LCP 0.2s, accès sensibles bloqués
### Completion Notes List ### Completion Notes List
- Page canary créée avec titre animé, badges, card de test, boutons - Page canary créée avec titre animé, badges, card de test, boutons
- CSS regénéré (12 Ko minifié) - CSS régénéré via `npm run build`
- Dépendances PHP installées (vlucas/phpdotenv) - Dépendances PHP installées (composer --no-dev)
- Configuration nginx exemple créée (nginx.conf.example) - Tests: `powershell -ExecutionPolicy Bypass -File tests/run.ps1`
- Syntaxe PHP validée
**Tâches restantes (manuelles) :** **Tâches restantes (manuelles) :**
- Créer .env de production sur le serveur - Créer .env de production sur le serveur
- Copier nginx.conf.example et adapter pour votre serveur
- Déployer les fichiers (rsync/FTP)
- Configurer SSL/HTTPS
- Tests de sécurité et performance
### File List ### File List
| Fichier | Action | | Fichier | Action |
|---------|--------| |---------|--------|
| `index.php` | Modifié | | `index.php` | Modifié |
| `nginx.conf.example` | Créé |
| `vendor/` | Installé |
| `composer.lock` | Créé |
| `assets/css/output.css` | Regénéré | | `assets/css/output.css` | Regénéré |
| `tests/canary.test.ps1` | Créé |
| `tests/run.ps1` | Modifié |
| `tests/structure.test.ps1` | Modifié |
| `composer.json` | Modifié |
| `composer.lock` | Créé |
| `nginx.conf.example` | Créé |
## QA Results ## QA Results

11
includes/functions.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
/**
* Inclut un template avec des données
* @param string $name Nom du template (sans .php)
* @param array $data Variables à passer au template
*/
function include_template(string $name, array $data = []): void
{
extract($data, EXTR_SKIP);
include __DIR__ . "/../templates/{$name}.php";
}

View File

@@ -1,11 +1,48 @@
<!DOCTYPE html> <?php
<html lang="fr"> // index.php - Page Canary
<head>
<meta charset="UTF-8" /> require_once __DIR__ . '/includes/functions.php';
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello World</title> include_template('header', [
</head> 'pageTitle' => 'Portfolio en construction',
<body> 'pageDescription' => 'Mon portfolio de développeur web arrive bientôt. Restez connectés !'
<h1>Hello World</h1> ]);
</body> ?>
</html>
<main class="min-h-screen flex items-center justify-center">
<div class="container-content text-center py-20">
<h1 class="text-display text-text-primary mb-4 animate-fade-in">
Portfolio <span class="text-primary">en construction</span>
</h1>
<p class="text-xl text-text-secondary mb-8 max-w-2xl mx-auto animate-fade-in animation-delay-100">
Je prépare quelque chose de génial pour vous.
<br>Revenez bientôt pour découvrir mes projets !
</p>
<div class="flex justify-center gap-4 mb-12 animate-fade-in animation-delay-200">
<span class="badge">PHP</span>
<span class="badge">Tailwind CSS</span>
<span class="badge badge-primary">En cours</span>
</div>
<div class="card max-w-md mx-auto animate-fade-in animation-delay-300">
<div class="card-body">
<h3 class="text-subheading mb-2">Infrastructure validée</h3>
<p class="text-text-secondary mb-4">
PHP, Tailwind CSS et le serveur fonctionnent correctement.
</p>
<div class="flex gap-4 justify-center">
<span class="btn-primary">Bouton Primary</span>
<span class="btn-secondary">Bouton Secondary</span>
</div>
</div>
</div>
<p class="text-text-muted text-sm mt-12">
Testé sur mobile, tablette et desktop.
</p>
</div>
</main>
<?php include_template('footer'); ?>

58
nginx.conf.example Normal file
View File

@@ -0,0 +1,58 @@
server {
listen 80;
server_name codex.skycel.me www.codex.skycel.me;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name codex.skycel.me www.codex.skycel.me;
# SSL
ssl_certificate /etc/letsencrypt/live/codex.skycel.me/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/codex.skycel.me/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/codex.skycel.me;
index index.php;
charset utf-8;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Block sensitive files
location ~ /\.(env|git|htaccess) { deny all; return 404; }
location ^~ /vendor/ { deny all; return 404; }
location ^~ /node_modules/ { deny all; return 404; }
location ^~ /logs/ { deny all; return 404; }
location ^~ /data/ { deny all; return 404; }
location ^~ /includes/ { deny all; return 404; }
# Static assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
gzip_static on;
}
# Front controller
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml;
}

19
templates/footer.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
/**
* Template Footer
*/
$currentYear = date('Y');
?>
<!-- Footer -->
<footer class="bg-surface border-t border-border py-8 mt-auto">
<div class="container-content text-center">
<p class="text-text-muted text-sm">
&copy; <?= $currentYear ?> Portfolio. Tous droits réservés.
</p>
</div>
</footer>
<!-- Scripts -->
<script src="/assets/js/main.js" defer></script>
</body>
</html>

38
templates/header.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
/**
* Template Header
* Variables disponibles :
* - $pageTitle (string) : Titre de la page
* - $pageDescription (string, optionnel) : Meta description
*/
$pageTitle = $pageTitle ?? 'Portfolio - Développeur Web';
$pageDescription = $pageDescription ?? 'Portfolio de développeur web full-stack. Découvrez mes projets, compétences et parcours.';
$siteName = 'Portfolio';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="<?= htmlspecialchars($pageDescription, ENT_QUOTES, 'UTF-8') ?>">
<!-- Open Graph -->
<meta property="og:title" content="<?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?>">
<meta property="og:description" content="<?= htmlspecialchars($pageDescription, ENT_QUOTES, 'UTF-8') ?>">
<meta property="og:type" content="website">
<meta property="og:locale" content="fr_FR">
<!-- Preload fonts -->
<link rel="preload" href="/assets/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/jetbrains-mono-var.woff2" as="font" type="font/woff2" crossorigin>
<!-- Favicon -->
<link rel="icon" href="/assets/img/favicon.ico" type="image/x-icon">
<!-- CSS -->
<link rel="stylesheet" href="/assets/css/output.css">
<title><?= htmlspecialchars($pageTitle, ENT_QUOTES, 'UTF-8') ?> | <?= $siteName ?></title>
</head>
<body class="bg-background text-text-primary font-sans antialiased">

18
tests/canary.test.ps1 Normal file
View File

@@ -0,0 +1,18 @@
$ErrorActionPreference = 'Stop'
function Assert-True {
param(
[bool]$Condition,
[string]$Message
)
if (-not $Condition) { throw $Message }
}
Assert-True (Test-Path 'index.php') 'Missing index.php'
$index = Get-Content -Raw 'index.php'
Assert-True ($index -match 'Portfolio') 'index.php missing Portfolio text'
Assert-True ($index -match 'text-primary') 'index.php missing text-primary class'
Assert-True ($index -match 'badge') 'index.php missing badge class'
Assert-True ($index -match 'btn-primary') 'index.php missing btn-primary class'
'OK'

View File

@@ -2,4 +2,6 @@
$here = Split-Path -Parent $MyInvocation.MyCommand.Path $here = Split-Path -Parent $MyInvocation.MyCommand.Path
& (Join-Path $here 'structure.test.ps1') & (Join-Path $here 'structure.test.ps1')
& (Join-Path $here 'tailwind.test.ps1') & (Join-Path $here 'tailwind.test.ps1')
& (Join-Path $here 'templates.test.ps1')
& (Join-Path $here 'canary.test.ps1')
'OK' 'OK'

View File

@@ -30,8 +30,14 @@ Assert-True (Test-Path 'logs/.gitkeep') 'Missing logs/.gitkeep'
Assert-True (Test-Path 'index.php') 'Missing index.php' Assert-True (Test-Path 'index.php') 'Missing index.php'
$index = Get-Content -Raw 'index.php' $index = Get-Content -Raw 'index.php'
Assert-True ($index -match 'Hello World') 'index.php missing Hello World' if (-not ($index -match 'Hello World')) {
Assert-True ($index -match 'meta name="viewport"') 'index.php missing viewport meta' Assert-True ($index -match 'Portfolio') 'index.php missing expected content'
}
if (-not ($index -match 'meta name="viewport"')) {
Assert-True (Test-Path 'templates/header.php') 'Missing templates/header.php for viewport meta'
$header = Get-Content -Raw 'templates/header.php'
Assert-True ($header -match 'meta name="viewport"') 'Header missing viewport meta'
}
Assert-True (Test-Path '.gitignore') 'Missing .gitignore' Assert-True (Test-Path '.gitignore') 'Missing .gitignore'
$gitignore = Get-Content -Raw '.gitignore' $gitignore = Get-Content -Raw '.gitignore'
@@ -40,7 +46,6 @@ $required = @(
'vendor/', 'vendor/',
'node_modules/', 'node_modules/',
'logs/*.log', 'logs/*.log',
'assets/css/output.css',
'.idea/', '.idea/',
'.vscode/', '.vscode/',
'.DS_Store' '.DS_Store'

33
tests/templates.test.ps1 Normal file
View File

@@ -0,0 +1,33 @@
$ErrorActionPreference = 'Stop'
function Assert-True {
param(
[bool]$Condition,
[string]$Message
)
if (-not $Condition) { throw $Message }
}
Assert-True (Test-Path 'includes/functions.php') 'Missing includes/functions.php'
$functions = Get-Content -Raw 'includes/functions.php'
Assert-True ($functions -match 'function\s+include_template') 'Missing include_template function'
Assert-True (Test-Path 'templates/header.php') 'Missing templates/header.php'
$header = Get-Content -Raw 'templates/header.php'
Assert-True ($header -match '<!DOCTYPE html>') 'Header missing doctype'
Assert-True ($header -match 'meta name="viewport"') 'Header missing viewport meta'
Assert-True ($header -match 'output\.css') 'Header missing output.css link'
Assert-True ($header -match '\$pageTitle') 'Header missing pageTitle'
Assert-True (Test-Path 'templates/footer.php') 'Missing templates/footer.php'
$footer = Get-Content -Raw 'templates/footer.php'
Assert-True ($footer -match 'main\.js') 'Footer missing main.js'
Assert-True ($footer -match '</body>') 'Footer missing closing body'
Assert-True (Test-Path 'index.php') 'Missing index.php'
$index = Get-Content -Raw 'index.php'
Assert-True ($index -match 'include_template\(') 'index.php missing include_template usage'
Assert-True (Test-Path 'assets/js/main.js') 'Missing assets/js/main.js'
'OK'