# Spec de référence — Module « Pilotage & Analytics » (05/08/2026)

**Statut** : spécification fournie par Enguerran pour le cahier des charges d'évolution globale.
Elle **remplace partiellement les livrables de conception P1.6** (schéma de tracking) **et P1.4**
(définitions des KPI) du plan d'action : ce qui est écrit ici est figé, seuls les paramètres
entre [crochets] restent à valider (cf. §Points à trancher en atelier).

Le prompt original est reproduit verbatim en §2. Le §1 le réconcilie avec le backlog.

---

## 1. Réconciliation avec le backlog (`03_BACKLOG_ISSUES.md`)

| Spec | Contenu | Issue(s) | Ajustement apporté |
|---|---|---|---|
| Lot 1.1 | `lesson_views` (schéma, index, POST /api/track/lesson-view, anti-bruit [30 s], insertion systématique) | EA-011 | Schéma et règles repris tels quels |
| Lot 1.1 (durée) | `duration_seconds` fin de lecture (heartbeat / sendBeacon best effort) | EA-012 | Méthode de mesure figée (plus besoin de la trancher en P1.6) |
| Lot 1.2 | `resource_downloads` + route authentifiée + blocage accès direct + `kind=certificate` | EA-004, EA-005 | Ajout : journaliser aussi les certificats (`kind`) ; « 3 endroits front » à migrer |
| Lot 1.3 | `login_events` (ip tronquée, user_agent, au moment de la création du token) | EA-010 | Schéma figé |
| Lot 1.4 | `quiz_attempts` sans supprimer `quiz_scores` (rétro-compat dernière tentative) | EA-013 | Stratégie de rétro-compat confirmée |
| Lot 1.5 | Exposer `completed_at` (learner/detail + export XLSX) | EA-016 | Endpoints cibles précisés |
| Lot 2.1 | Vue « Modules » (funnel, vues, uniques, temps moyen, téléchargements, score moyen) | EA-055 | Colonnes du dashboard figées |
| Lot 2.2 | Vue « Apprenants » nominatif + **profil d'usage** (Jamais actif / One-shot / Occasionnel / Récurrent) + timeline | EA-049, EA-051 | Ajout du badge de profil et de la timeline |
| Lot 2.3 | Vue « Documents » (téléchargements par fichier × apprenant) | **EA-082 (nouvelle)** | Issue créée |
| Lot 2.4 | Exports XLSX/CSV avec filtres actifs | EA-022, EA-023 | Confirmé |
| Lot 2.5 | **Définitions de KPI figées** (Démarré, Récurrence, Taux de complétion, exclusions) | EA-016, EA-018, EA-055 | Reprises dans les CA — la « revue d'indicateurs » P1.4 se limite aux KPI NON couverts ici |
| Lot 3 | Bugs compteurs (périmètre, unique(), break, is_complete, is_active, per_page) | EA-009, EA-001, EA-014 | Iso avec le backlog — aucun écart |
| Transverses | RGPD (IP /24, rétention [24 mois], purge, droit d'accès/effacement), perf (index, pas de N+1), rétro-compat, recette bout-en-bout | EA-017 + CA des issues Lot 1/2 | Scénario de recette repris comme critère de sortie du Sprint 2 |

**Scénario de recette (critère de sortie Sprint 2, repris de la spec)** : un apprenant test ouvre
3 leçons sur 2 jours, télécharge 1 ressource, passe 2 fois un quiz, termine 1 module → chaque
dashboard reflète exactement ces événements (2 jours actifs → profil « Occasionnel », 3 vues,
1 téléchargement, 2 tentatives, 1 complétion datée).

**Note d'arbitrage (reprise de la spec)** : si le budget impose de phaser, le Lot 1 seul a déjà de
la valeur (les données s'accumulent dès la mise en prod). L'ordre inverse n'a aucun sens —
cohérent avec le séquencement du plan (CH1 = S1-S2, restitution = S6).

### Pièges d'implémentation à nommer dans les tickets

**Couplage dédoublonnage ↔ durée (Lot 1.1).** La vue et sa durée sont écrites en deux temps
(ouverture, puis fermeture via heartbeat/beacon). Quand la fenêtre anti-bruit écarte une ouverture
comme doublon, la durée qui arrive ensuite doit se rattacher à la **ligne conservée** : ni ligne
orpheline, ni écrasement d'une durée déjà mesurée. Conception recommandée : le front génère un
**identifiant de vue** au montage du lecteur et le renvoie avec la durée ; le back résout cet
identifiant vers la ligne retenue, plutôt que de retrouver « la dernière vue de ce couple
user×leçon », qui est ambigu dès qu'il y a concurrence (deux onglets ouverts).
Cas de recette : 3 ouvertures en 10 s → 1 ligne et 1 durée ; 2 consultations réellement espacées →
2 lignes et 2 durées ; durée arrivant après un doublon écarté → aucune ligne supplémentaire ;
durée en retard → n'écrase jamais une valeur déjà supérieure. Repris dans EA-012.

### Arbitrage outillage : journal en base plutôt qu'un outil d'analytics tiers

Question posée le 05/08 : peut-on s'appuyer sur un outil d'analytics existant (Matomo, PostHog,
Mixpanel, GA4…) plutôt que de construire les tables d'événements ?

**Décision : non pour ce besoin, journal en base comme prévu par la spec.** Trois raisons :

1. **Le besoin n'est pas de l'analytics produit, c'est du reporting métier nominatif restitué
   dans l'application** — cockpit par apprenant, périmètre hiérarchique (`parent_id`), exports.
   Les outils d'analytics sont conçus pour l'exploration agrégée par une équipe interne, pas pour
   exposer des données par personne à des utilisateurs finaux avec des droits ligne à ligne.
2. **Les KPI exigent des jointures avec le domaine** (inscriptions, complétions, tentatives,
   hiérarchie). « Taux de complétion = terminés ÷ inscrits, période et périmètre appliqués aux deux
   termes » suppose de connaître `enrollments` : un outil externe ne l'a pas, il faudrait lui
   pousser tout le domaine ou rapatrier les événements — plus coûteux qu'une table.
3. **RGPD** : il s'agit de l'activité de formation de salariés, nominative. Ajouter un
   sous-traitant (a fortiori hors UE : Mixpanel, Amplitude, GA4) ouvre un dossier DPA et transferts
   pour un gain nul sur le besoin exprimé.

À noter : le coût du journal lui-même est faible (4 tables et quelques endpoints) ; **l'essentiel
de la charge est dans les écrans de restitution**, qui restent à construire quel que soit le lieu
de stockage des événements.

**Complément possible, à distinguer** — un outil d'analytics produit reste pertinent pour un autre
usage : comprendre l'usage de la plateforme elle-même (parcours dans l'interface, abandons) pour
nos propres décisions produit. Dans ce cas : instance auto-hébergée ou UE (Matomo, PostHog),
identifiants pseudonymes, **aucune donnée nominative de formation**. Sujet distinct, non requis ici.

**Optionalité peu coûteuse à conserver** : nommer les événements dans un vocabulaire proche de
**xAPI** (acteur / verbe / objet — `viewed`, `completed`, `attempted`, `downloaded`). Cela ne coûte
rien aujourd'hui et facilitera demain l'export vers un LRS ou l'interopérabilité multi-LMS,
cohérente avec la vision d'un pipeline LMS-agnostique. Adopter un LRS maintenant serait en revanche
prématuré : c'est une infrastructure de plus, et cela ne réglerait pas la question des écrans.

### Paramètres tranchés en atelier (05/08 — cf. `06_DECISIONS_ATELIER_P0.md`, fiche F11)

| Paramètre | Décision |
|---|---|
| Fenêtre anti-bruit des vues | **30 s**, **paramétrable par l'admin** (pas de constante en dur) |
| Rétention des événements bruts | **24 mois**, **paramétrable par l'admin**, purge planifiée |
| Comptes « test » exclus des compteurs | **L'admin sélectionne lui-même les comptes** dans l'interface — pas de liste figée en configuration |
| Volumétrie cible | **500 – 5 000** apprenants — confirmée |
| Mention d'information RGPD | À rédiger (responsable à désigner) |

**Profils d'usage — deux besoins distincts (décision F11).** Le profil (`Jamais actif` /
`One-shot` / `Occasionnel` / `Récurrent`) **qualifie l'utilisateur au niveau de la plateforme**,
toutes formations **et tutos** confondus. **En parallèle**, le cockpit doit offrir la **vision par
formation** — ce sont deux restitutions différentes, pas deux calculs concurrents.
Seuils inchangés (1 jour actif / 2-3 / ≥ 4 jours actifs distincts).
Période de calcul : **90 jours glissants par défaut, paramétrable** — valeur proposée et non
tranchée explicitement dans les retours ; elle évite qu'un utilisateur actif il y a deux ans reste
« récurrent » indéfiniment.

**Restitutions ajoutées par les retours d'atelier :**
- **Vue tutos différenciée** dans le cockpit : nombre de tutos visionnés, temps passé, liste des
  tutos visionnés — **sans** statut de formation ni résultat de quiz (les tutos n'en ont pas).
- **Formations les plus consultées**, tous utilisateurs confondus, **avec le détail par leçon**.

Ces deux points sont portés par l'issue **EA-085**.

---

## 2. Spécification verbatim (source : Enguerran, 05/08/2026)

# Prompt — Module « Pilotage & Analytics » pour Efektiv Académie

*À coller tel quel dans le cahier des charges d'évolution globale, ou à donner à l'équipe dev / à un assistant IA de développement. Adapter les sections entre [crochets] si besoin.*

## Contexte technique (existant, à ne pas casser)

Tu interviens sur le LMS **Efektiv Académie** :
- **Backend** : Laravel 12, MySQL, authentification API stateless via Sanctum (`personal_access_tokens`), rôles Spatie (`admin`, `manager`, `manager_general`, `director`, `sub_manager`, `teacher`, `learner`) avec périmètre hiérarchique par `users.parent_id`.
- **Frontend** : SPA React (Vite), repo séparé, API consommée via token Bearer stocké en localStorage.
- **Modèle pédagogique** : `learning_paths` → `courses` (= modules) → `sections` → `lessons` + `quizzes` (→ `questions`/`options`). Inscriptions dans `enrollments`, complétions dans `completed_courses` (avec `completed_at` et certificat), validations de leçons dans `learner_lessons` (une ligne par couple apprenant×leçon, créée au clic « Terminer la leçon »), scores dans `quiz_scores` (⚠ dernière tentative uniquement, les précédentes sont supprimées).
- **Ressources téléchargeables** : `course_files` (upload admin), aujourd'hui servies en **lien statique public** (`/storage/course_resources/...`) sans passer par Laravel.

## Objectif

Doter la plateforme d'un **module de pilotage** répondant nominativement et par module aux questions : qui consulte quoi, quand, à quelle fréquence (one-shot vs récurrent), qui télécharge quels documents, avec quels résultats (quiz, complétions) — sans dégrader l'expérience apprenant ni les performances.

## Lot 1 — Collecte d'événements (socle, priorité absolue)

Créer une journalisation d'événements d'usage, nominatifs et horodatés :

1. **Table `lesson_views`** : `id, user_id (FK users), lesson_id (FK lessons), course_id (FK courses, dénormalisé), viewed_at, duration_seconds (nullable), source (web|mobile, nullable)`. Index `(course_id, viewed_at)` et `(user_id, viewed_at)`.
   - Écrite **à l'ouverture** de la leçon côté front (montage du composant de lecture), via `POST /api/track/lesson-view` — insertion systématique (pas d'updateOrCreate : on veut l'historique).
   - `duration_seconds` renseignée en fin de lecture (heartbeat ou beacon à la fermeture — `navigator.sendBeacon`), en best effort.
   - Anti-bruit : côté back, ignorer les vues du même user×lesson espacées de moins de [30] secondes.
2. **Table `resource_downloads`** : `id, user_id, course_file_id (FK), course_id, downloaded_at, kind (resource|certificate)`.
   - **Remplacer les liens statiques** par une route authentifiée `GET /api/course/file/{id}/download` qui journalise puis renvoie le fichier (`Storage::download` ou X-Sendfile). Mettre à jour le front (les 3 endroits qui construisent `VITE_APP_MEDIA_URL + stored_path`). Bloquer l'accès direct au dossier `storage/course_resources` (déplacer hors du disque `public` ou règle serveur).
   - Journaliser aussi les téléchargements de certificats dans la même table (`kind=certificate`), dans la route existante `/learner/certificates/download/{id}`.
3. **Table `login_events`** : `id, user_id, logged_at, ip_truncated, user_agent` — alimentée au login (là où le token Sanctum est créé). Ne pas dépendre des tokens (supprimés au logout).
4. **Historique des quiz** : nouvelle table `quiz_attempts` (`id, user_id, quiz_id, attempt_no, score_percent, submitted_at`) alimentée à chaque soumission, **sans supprimer** l'existant `quiz_scores` (qui reste la « dernière tentative » pour la rétro-compat).
5. **Exposer `completed_at`** : ajouter la date de complétion dans la réponse de `GET /manager/learner/detail/{id}` et dans l'export XLSX `POST /manager/users/export`.

Contraintes : écritures non bloquantes (queue Laravel si nécessaire), aucune régression sur les parcours actuels, migrations réversibles, seeds de démo, tests (unitaires sur la journalisation, feature sur la route de download authentifiée : un download crée exactement une ligne et renvoie bien le fichier).

## Lot 2 — Restitution (dashboards + exports)

Écrans d'administration (visibles admin ; manager/directeur/sub-manager restreints à leur périmètre `parent_id`) :

1. **Vue « Modules »** : tableau triable/filtrable par période — pour chaque module : inscrits, ayant démarré, ayant terminé, taux de complétion, **vues de leçons**, **apprenants uniques actifs**, temps moyen passé, téléchargements de ressources, score quiz moyen. Funnel inscrit → démarré → terminé.
2. **Vue « Apprenants » (nominatif)** : pour chaque apprenant — modules affectés/démarrés/terminés, certificats, dernière connexion, nombre de connexions, **jours actifs distincts**, et un badge de profil calculé : `Jamais actif` / `One-shot` (1 jour actif) / `Occasionnel` (2-3 jours) / `Récurrent` (≥ 4 jours) [seuils à valider]. Timeline d'activité individuelle (vues, quiz, complétions, téléchargements).
3. **Vue « Documents »** : téléchargements par fichier et par apprenant, avec dates.
4. **Exports** : chaque vue exportable en XLSX/CSV avec les filtres actifs (période, module, parcours, organisation/périmètre, profil d'usage).
5. **Définitions de KPI à figer** (les implémenter exactement ainsi) :
   - *Démarré* = ≥ 1 `lesson_view` OU ≥ 1 `learner_lesson` OU ≥ 1 tentative de quiz sur le module.
   - *Récurrence* = nombre de **jours calendaires distincts** avec au moins un événement d'usage.
   - *Taux de complétion* = apprenants ayant terminé ÷ apprenants inscrits (période et périmètre appliqués aux deux termes).
   - Les compteurs excluent par défaut les comptes de rôle ≠ learner et les comptes marqués « test » [liste à fournir].

## Lot 3 — Corrections des compteurs existants (dans la même vague)

- `DashBoardController::index` (mode `course_id`) : transmettre le périmètre managérial à `getSingleCourseStats` (aujourd'hui ignoré — signature à un seul paramètre).
- `CourseCompleRation` : rétablir la déduplication (`unique()` commentés) pour ne plus compter un apprenant N fois.
- `sidebarStatics` : ajouter les `break` manquants (`sub_manager`, `manager_general` tombent dans `director`).
- `EnrollmentController` : supprimer l'écriture vers `enrollments.is_complete` (colonne inexistante) ou créer la colonne.
- `users.is_active` : créer la migration manquante (colonne actuellement hors migrations).
- `UserController::index` : respecter `per_page` (pagination figée à 20 aujourd'hui).

## Exigences transverses

- **RGPD** : IP tronquée (/24) ou hashée dans `login_events` ; durée de rétention paramétrable [par défaut 24 mois] avec purge planifiée ; mention dans la politique de confidentialité ; accès aux données nominatives limité aux rôles habilités ; export/suppression sur demande (droit d'accès/effacement) couvrant les nouvelles tables.
- **Performance** : volumétrie cible [500-5 000] apprenants ; index cités ; pas de requête N+1 dans les dashboards (agrégations SQL, pas de boucles PHP) ; pagination systématique.
- **Rétro-compatibilité** : aucun changement de comportement pour l'apprenant hormis le lien de téléchargement ; les YAML/flux existants et les écrans actuels continuent de fonctionner pendant la transition.
- **Livrables** : migrations + modèles + routes + contrôleurs testés ; écrans React ; documentation courte (schéma des tables, définitions des KPI, guide d'usage des dashboards) ; jeu de données de démonstration.
- **Recette** : scénario bout-en-bout — un apprenant test ouvre 3 leçons sur 2 jours, télécharge 1 ressource, passe 2 fois un quiz, termine 1 module → chaque dashboard reflète exactement ces événements (2 jours actifs → profil « Occasionnel », 3 vues, 1 téléchargement, 2 tentatives, 1 complétion datée).

*Note d'arbitrage : si le budget impose de phaser, le Lot 1 seul a déjà de la valeur (les données s'accumulent dès la mise en prod, même si la restitution vient plus tard). L'ordre inverse n'a aucun sens : sans collecte, rien à restituer.*
