diff --git a/CLAUDE.md b/CLAUDE.md
index ca893a5..44f1936 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -49,8 +49,8 @@ appliquée manuellement en SQL — il n'y a pas d'outil de migration.
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` :
+**Pages HTML** — `dashboard.php`, `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';
@@ -74,8 +74,11 @@ 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`**.
+partiel `app/Views/partials/` + module JS + **inscription de la clé dans le tableau `$navigation` de
+`app/Views/layouts/header.php`, dans un groupe de `$navigationGroups`, et dans `acces_role_page` par
+une migration SQL** (les pages autorisées sont lues en base, pas écrites en PHP — voir plus bas).
+Une page absente des groupes reste affichée dans un groupe « Autres » : le filet évite qu'un oubli
+la rende inatteignable. Chaque page passe aussi un `pageSubtitle` décrivant l'action de l'écran.
### Chaîne d'un appel
@@ -108,6 +111,11 @@ SQL dans `acces_role_page` / `acces_role_permission`, sans redéploiement. Une p
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.
+`dashboard.php` (Service Enfance uniquement, voir `README_V7_9.md`) illustre la règle : le droit
+d'accès vient de `database/migration_v7_9_tableau_de_bord.sql`. Tant que la migration n'est pas
+jouée, l'onglet n'apparaît pas et `Access::homeUrl()` se replie sur `planning.php` — ce repli est
+indispensable, sans lui `requirePage()` renverrait indéfiniment vers une page interdite.
+
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
@@ -145,9 +153,10 @@ PTA.modules.agentOverview = { init, load };
applique les filtres d'URL (`structure_id`, `agent_id`, `week`, `month`, `focus`). Les modules se
rafraîchissent entre eux via l'appel optionnel `PTA.modules.autre?.refresh?.({ silent: true })`.
-Les assets sont versionnés à la main : `assets/style.css?v=7.7` et `assets/js/*.js?v=7.7` dans
-`layouts/header.php` / `layouts/footer.php`. **Bumper ce numéro après toute modification de JS/CSS**,
-sinon les navigateurs et le `expires 7d` de nginx servent l'ancienne version.
+Les assets sont versionnés à la main : `assets/style.css?v=7.9` et `assets/js/*.js?v=7.9` dans
+`layouts/header.php` / `layouts/footer.php` **et dans `Views/pages/role.php`**, qui porte sa propre
+balise `
`. **Bumper ce numéro aux trois endroits après toute modification de JS/CSS**, sinon
+les navigateurs et le `expires 7d` de nginx servent l'ancienne version.
### Modèle métier
diff --git a/README_V7_9.md b/README_V7_9.md
new file mode 100644
index 0000000..560dd06
--- /dev/null
+++ b/README_V7_9.md
@@ -0,0 +1,113 @@
+# PTA V7.9 — Parcours et lisibilité du Service Enfance
+
+## Le problème traité
+
+Le rôle **Service Enfance** ouvre onze écrans. Jusqu'ici :
+
+- il atterrissait sur **Saisie**, alors que son travail commence par le contrôle et non par la saisie ;
+- les onze onglets étaient alignés à plat, sans hiérarchie : rien ne distinguait une tâche
+ quotidienne (« À valider ») d'un référentiel modifié deux fois par an (« Administration ») ;
+- chaque bandeau de page affichait le **même** sous-titre générique, il fallait donc ouvrir un écran
+ pour savoir à quoi il servait ;
+- les chiffres qui déclenchent une action (brouillons en attente, manques de couverture, quotas)
+ vivaient sur trois écrans distincts : personne ne voyait l'état général sans faire le tour ;
+- la pastille « À valider » de la barre de navigation n'était alimentée que **sur la page
+ « À valider » elle-même**, donc invisible là où elle aurait servi.
+
+## Ce qui change
+
+### 1. Un tableau de bord comme page d'accueil
+
+Nouvelle page `dashboard.php`, **réservée au Service Enfance**, qui devient sa page d'atterrissage.
+
+- **Quatre tuiles d'action**, vertes quand il n'y a rien à faire, rouges dès qu'un compteur remonte :
+ plannings à valider, manques de couverture de la semaine, quotas dépassés, lieux sans horaires de
+ couverture. Chaque tuile est un lien vers l'écran de traitement.
+- **« À valider en priorité »** : les cinq brouillons dont la semaine est la plus ancienne, avec un
+ bouton **Contrôler** qui ouvre directement l'écran de saisie sur le bon agent, le bon lieu, la
+ bonne semaine, curseur sur la validation.
+- **« Couverture à renforcer »** : les cinq plages les plus déficitaires, triées par nombre d'agents
+ manquants, avec un lien vers l'écran de couverture déjà filtré.
+- **« Suivi des quotas annuels »** : consommation globale, reste à affecter, agents sans aucune heure,
+ et le détail par agent avec une jauge et un lien vers sa vue annuelle.
+- **« Actions fréquentes »** : six raccourcis vers les écrans les plus utilisés.
+
+Un sélecteur de semaine en haut de page rejoue l'ensemble des indicateurs de couverture, et indique
+si la semaine est scolaire, de vacances, ou à cheval sur les deux.
+
+### 2. Navigation regroupée par intention
+
+Les onglets sont désormais rangés en quatre groupes : **Piloter** (tableau de bord, à valider,
+couverture), **Planifier** (saisie, équipes), **Consulter** (par agent, par lieu, vue annuelle, PTA
+détaillé), **Paramétrer** (vacances, administration).
+
+Les rôles qui n'ont que quelques pages — Agent (2), Responsable de structure (4) — gardent la barre
+simple d'avant, sans titres de groupe. Une page autorisée en base mais absente des groupes reste
+affichée dans un groupe « Autres » plutôt que de disparaître.
+
+### 3. Chaque page dit ce qu'on y fait
+
+Le sous-titre du bandeau est propre à chaque écran et décrit l'action, pas le produit.
+
+### 4. La pastille « À valider » suit partout
+
+Elle est alimentée depuis n'importe quel écran par un compteur léger
+(`api/dashboard_summary.php?scope=badges`). L'échec de cet appel est silencieux : une commodité ne
+doit jamais empêcher une page de fonctionner.
+
+## Ce qui ne change pas
+
+Aucune règle métier, aucun calcul, aucun droit n'est modifié.
+
+Le tableau de bord ne fait que **relire** ce que les autres écrans calculent déjà : la file de
+validation vient de `PlanningModel::pendingValidation()`, la couverture de
+`CoverageModel::reportForStructure()`, et le quota reprend exactement la logique de `QuotaModel`
+(année civile, motifs décomptés uniquement, quota de l'année la plus proche à défaut de l'année
+exacte) — un agent ne peut donc pas afficher deux valeurs différentes selon l'écran consulté.
+
+L'endpoint `api/dashboard_summary.php` est gardé par `requireService()` : 403 pour les autres rôles,
+401 sans session.
+
+## Migration
+
+```sql
+SOURCE database/migration_v7_9_tableau_de_bord.sql;
+```
+
+Elle insère une seule ligne dans `acces_role_page`. **Elle doit être jouée en même temps que le
+déploiement du code**, dans un sens comme dans l'autre :
+
+- code déployé, migration non jouée → l'onglet n'apparaît pas, le Service Enfance atterrit comme
+ avant sur l'écran de saisie, et `dashboard.php` renvoie vers l'accueil. L'application reste
+ entièrement fonctionnelle, simplement sans tableau de bord ;
+- migration jouée, code non déployé → l'onglet apparaît et pointe vers un fichier inexistant.
+
+Retour arrière :
+
+```sql
+DELETE FROM acces_role_page WHERE role = 'SERVICE_ENFANCE' AND page = 'dashboard';
+```
+
+## Fichiers
+
+| Ajouté | Rôle |
+| --- | --- |
+| `dashboard.php` | point d'entrée de la page |
+| `api/dashboard_summary.php` | endpoint JSON |
+| `app/Controllers/DashboardController.php` | agrégation et gardes d'accès |
+| `app/Models/DashboardModel.php` | suivi des quotas et compteurs |
+| `app/Views/pages/dashboard.php`, `app/Views/partials/dashboard.php` | vue |
+| `assets/js/dashboard.js` | module `PTA.modules.dashboard` |
+| `database/migration_v7_9_tableau_de_bord.sql` | droit d'accès à la page |
+
+| Modifié | Raison |
+| --- | --- |
+| `app/Core/Access.php` | `homeUrl()` vise le tableau de bord, avec repli si la migration manque |
+| `app/Controllers/PageController.php` | méthode `dashboard()` et sous-titre propre à chaque page |
+| `app/Views/layouts/header.php` | navigation regroupée, sous-titre contextuel |
+| `app/Views/layouts/footer.php`, `app/Views/pages/role.php` | versions d'assets `?v=7.9` |
+| `assets/js/page-bootstrap.js` | pastille « À valider » sur toutes les pages |
+| `assets/style.css` | styles de la navigation groupée et du tableau de bord |
+
+Les jauges de quota utilisent l'élément natif `