# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Contexte PTA (Planning de Travail Annualisé) — application PHP 8.3 multipage (MVC maison) pour la planification annualisée des agents d'accueils de loisirs périscolaires/extrascolaires. **Aucune dépendance** : pas de Composer, pas de npm, pas de framework, pas de bundler, pas de suite de tests. Uniquement PHP + PDO/MySQL + JavaScript vanilla. Ne pas introduire de dépendance sans raison forte : le PDF (`app/Services/SimplePdf.php`) et l'autoloader (`app/autoload.php`) sont volontairement réécrits à la main. **Tout est en français** : identifiants SQL (`creneau_horaire`, `id_agent`, `statut`), messages utilisateur, commentaires. Conserver cette langue pour tout nouveau code. ## Commandes ```bash # Démarrage complet (nginx :8080, phpMyAdmin :8081, MariaDB, php-fpm) cp .env.example .env # puis renseigner DB_PASSWORD / DB_ROOT_PASSWORD docker compose up -d --build # IMPORTANT : le code source est COPIÉ dans l'image au build (Dockerfile), il n'est pas monté. # Toute modification PHP/JS/CSS exige un rebuild des DEUX services : # (nginx sert lui-même les assets statiques depuis sa propre copie de /var/www/html) docker compose up -d --build php nginx docker compose logs -f php nginx db docker compose ps docker compose down # arrêt en conservant les données docker compose down -v # DÉTRUIT le volume MariaDB # Accès SQL direct docker compose exec db mariadb -upta -p pta # Vérification syntaxique (il n'existe ni linter ni tests automatisés) find . -name '*.php' -not -path './.git/*' -exec php -l {} \; ``` Le schéma `database/pta_prod_schema_mariadb.sql` n'est joué **qu'à la création initiale du volume** MariaDB (`docker-entrypoint-initdb.d`). Sur une base existante, toute évolution de schéma doit être appliquée manuellement en SQL — il n'y a pas d'outil de migration. ## Architecture ### Deux types de points d'entrée, tous à la racine ou dans `api/` Il n'y a pas de routeur. Chaque URL correspond à un fichier PHP réel de 5 à 10 lignes. **Pages HTML** — `planning.php`, `agents.php`, `annual.php`, `pta.php`, `teams.php`, `coverage.php`, `structures.php`, `pending.php`, `vacations.php`, `administration.php` : ```php require __DIR__ . '/page_bootstrap.php'; runPage('planning'); // -> PageController::planning() ``` `page_bootstrap.php` attrape les `Throwable` et affiche une page d'erreur HTML. **Endpoints JSON/PDF** — `api/*.php` plus `save_planning.php`, `validate_planning.php`, `reopen_planning.php` (ces trois-là sont restés à la racine pour raisons historiques) : ```php JsonResponse::bootstrap(); // handlers d'erreur -> JSON, jamais du HTML $pdo = require dirname(__DIR__) . '/app/bootstrap.php'; (new PlanningController($pdo))->show(); ``` `JsonResponse::bootstrap()` convertit warnings et exceptions en réponses JSON 500 — indispensable, car le JS refuse toute réponse non-JSON. **Les endpoints PDF ne l'appellent pas** (ils émettent des octets binaires via `PdfResponse::inline()`). Pour ajouter un endpoint : créer le fichier mince dans `api/`, ajouter la méthode au contrôleur. Pour ajouter une page : fichier racine + méthode dans `PageController` + vue `app/Views/pages/` + partiel `app/Views/partials/` + module JS + **inscription de la clé dans `Access::allowedPages()` et dans le tableau `$navigation` de `app/Views/layouts/header.php`**. ### Chaîne d'un appel ``` api/x.php → Controller (Access + Csrf + Request) → Model (SQL PDO) → JsonResponse::send() page.php → PageController → Models → Views/pages/x.php ├── layouts/header.php (nav filtrée par rôle) ├── partials/x.php (markup) └── layouts/footer.php (injecte window.PTA_CONFIG + scripts) ``` `app/bootstrap.php` démarre la session, ouvre le PDO (`db.php` ← `config.php` ← variables d'environnement `PTA_DB_*`) et appelle `SchemaModel::assertCurrent()` **à chaque requête**. Ajouter une table ou une colonne obligatoire impose de mettre à jour les listes de `SchemaModel`, sinon l'application entière refuse de démarrer. ### Rôles et périmètres (`app/Core/Access.php`) Trois rôles stockés en session : `AGENT` (lecture seule sur soi), `RESPONSABLE_STRUCTURE` (édition des brouillons de son seul lieu), `SERVICE_ENFANCE` (tout, seul habilité à valider). `role.php` est un **sélecteur de rôle, pas une authentification** — l'utilisateur choisit librement son identité. C'est la faille ouverte la plus importante du projet ; toute évolution vers la production doit remplacer cette sélection par un compte authentifié, sans toucher au reste de la mécanique de droits. **`Access.php` ne contient aucune règle de droit** : tout est lu dans les vues SQL créées par `database/migration_v7_8_securite_acces.sql` (voir `README_V7_8.md`). Modifier un droit se fait en SQL dans `acces_role_page` / `acces_role_permission`, sans redéploiement. Une permission absente de la base vaut « refusé ». `SchemaModel` vérifie la présence des sept vues au démarrage et fait échouer l'application si la migration manque — c'est volontairement fail-closed. Les contrôles sont doublés : l'UI masque, mais le serveur tranche. Toute méthode de contrôleur commence par un garde `requireService()`, `requireDraftEditor()`, `requireStructureEdit($id)`, `requireAgentView($id)`, `requireEntryEdit($id)`, `requirePlanningEdit($id)`… Ces gardes émettent directement un 401/403 JSON et coupent l'exécution. **Ne jamais ajouter un endpoint mutant sans garde ni sans `Csrf::assertPayload($payload)`.** CSRF : le jeton voyage **dans le corps JSON** (`csrf_token`), pas dans un en-tête. Côté JS : `csrf_token: window.PTA_CONFIG.csrfToken`. ### Contraintes de sécurité à respecter - **CSP à nonce** (`app/Core/Security.php`) : `script-src 'self' 'nonce-…'`, sans `unsafe-inline`. Tout `