# Consignation — passage en production, 23/08/2026

> **Ce que c'est** : l'état du sujet « bascule en production » au soir du 23/08, écrit par
> le pilote de la session de déploiement, **pour être lu par quelqu'un d'autre** — une
> session neuve, un associé, ou moi-même dans trois jours.
>
> **Ce que ce n'est pas** : un plan. Le plan est
> [`docs/roadmap/14_RUNBOOK_BASCULE_PROD.md`](../roadmap/14_RUNBOOK_BASCULE_PROD.md), §0
> fait foi. La procédure d'installation est
> [`docs/roadmap/25_INSTALLATION_SERVEUR_CIBLE.md`](../roadmap/25_INSTALLATION_SERVEUR_CIBLE.md).
>
> ⚠️ **Rien n'a été exécuté sur un serveur.** Tout ce qui suit vient de la lecture du code,
> du dépôt, des issues, et de mesures HTTP en lecture seule menées **sur mandat explicite**.
> Aucune session Claude n'atteint les serveurs (§4.1).
>
> ⚠️ **Une consignation périme.** Chaque affirmation ci-dessous porte sa date. Une mesure du
> 11/08 relue le 22/08 s'est révélée fausse (§2, décision 11) — c'est le mode de panne de ce
> genre de document, et la seule parade est de re-mesurer plutôt que de relire.

---

## 1. Où en est le sujet

### ✅ Fait — et vérifié, pas seulement annoncé

| quoi | preuve |
|---|---|
| **Merge `development` → `main`, les deux dépôts** | BACK PR **#621** (`967076e`) · FRONT PR **#347** (`4e88522`). Contrôle rejoué indépendamment : **identité d'arbre** (BACK `74eb060`, FRONT `0909f155`), `git diff origin/main origin/development` **vide**, 0 commit absent sur 719 (BACK) et 554 (FRONT). `main` porte octet pour octet la version recettée. |
| **Runbook de bascule révisé et mergé** | PR **#636**. Il était **resté 12 jours sur une branche non mergée** (`docs/181-runbook-bascule-prod`) pendant que #136, #301 et #307 le citaient comme référence. C'est le cas d'école de la leçon du 22/08. |
| **Procédure d'installation du serveur écrite et mergée** | PR **#637** → `docs/roadmap/25_INSTALLATION_SERVEUR_CIBLE.md` (333 lignes). ⚠️ **NON ÉPROUVÉE** tant que personne ne l'a jouée. |
| **Dimensionnement révisé à la baisse** | PR **#639** — CPX31 → **CPX21 (3 vCPU / 4 Go / 80 Go)**. Justification en §0.4 du runbook. |
| **Mandat de lecture sur la production exécuté** | A produit la confirmation **#618 VITAL** (§4.3). |
| **Inventaire du front** | 341 fichiers source, 99 écrans, 18 types d'objets d'interface dont **15 à fournisseur unique**. ⚠️ **Livré en conversation, PAS versé au dépôt** — c'est une dette de cette session (§3). |

### 🔄 En cours

| quoi | où |
|---|---|
| **Recette humaine sur staging** | pilotée hors de cette session |
| **PR de documentation ouvertes** | #638, #629, #620, #583, #572, #570, #455 — voir §4.4 |
| **Contradiction #575 / #626** | soulevée par un autre fil, **non tranchée** (§3) |

### ⛔ Non commencé — et c'est le chemin critique

| quoi | qui | bloque quoi |
|---|---|---|
| **#302 — SPF `include` Brevo + DKIM** dans la zone OVH | Enguerran | ⚠️ **le seul délai incompressible.** À lancer avant tout le reste |
| **#618 point 1** — couper l'accès public à `certificates/` | Enguerran / associé | rien ; c'est une fuite ouverte **maintenant** |
| **#618 point 2** — lire les journaux d'accès **avant** de corriger | Enguerran / associé | la correction efface la trace ; la mesure ne se rattrape pas |
| **Mesure du volume disque de la prod** + **récupération du `.htaccess` du front** | Enguerran / associé | confirme le gabarit 80 Go ; sans le `.htaccess`, tous les liens profonds du nouveau serveur tombent en 404 (#176, blocant B11) |
| **Abaisser le TTL DNS à 300 s** | Enguerran | à faire **plusieurs jours avant** la bascule |
| **Commande de la machine** | Enguerran | tout le reste |
| **Protéger `main`** dans l'interface GitHub | Enguerran | rend `merge_train.sh` inutile (§4.2) |
| **`release/2026-09` + bascule de `deploy-staging.yml`** | pilote, sur go | sépare développement et recette (§4.2) |
| **Script de conversion InnoDB versionné et testé** (B14 / #156) | pilote | l'étape 2 de la bascule |
| **Nettoyage du front, lots N1→N6** | pilote, sur go | rien de bloquant |
| **Issue de migration serveur** | non créée | la traçabilité du chantier |

---

## 2. Les décisions prises — et ce qui a été écarté

⚠️ **Une décision sans son écarté n'est pas consignée** : c'est l'alternative rejetée qui
empêche de la reprendre à zéro dans six semaines.

| # | décision | pourquoi | **écarté, et pourquoi** |
|---|---|---|---|
| 1 | **Serveur neuf monté à côté, puis bascule DNS** | le retour arrière devient « re-changer le DNS » | ⛔ la conversion **en place** en 90 min : on découvre les problèmes en ligne, sous pression, avec pour seul recours la restauration d'un dump. Ce n'est pas une optimisation du même plan, c'est **une autre classe de risque** |
| 2 | **Un serveur par usage** | profils de charge opposés | ⛔ le gros serveur partagé — c'est l'état OVH actuel (4 environnements, prod et staging **sur la même machine**, `57.129.1.122`). Microlearning fait du **rendu vidéo** : un rendu ferait ramer la plateforme des apprenants |
| 3 | **SaaS sur machine dédiée le moment venu** | **rayon d'impact et isolation des données**, pas puissance : le SaaS portera les données de plusieurs entreprises clientes ; et les rythmes sont opposés — un SaaS déploie souvent, une prod de formation doit être ennuyeuse | ⛔ le construire **aujourd'hui** : le SaaS n'existe pas (#594, #519). On adopte une architecture où il **pourra** être ajouté, pas une où il **devra** l'être |
| 4 | **Docker : NON en production** | il ajoute une couche à exploiter — images, volumes, réseau, un mode de panne de plus — et **personne n'exploite l'infra** | ⛔ Docker comme réponse à « tester depuis n'importe quelle branche » : **déjà résolu** par la CI (MySQL 8 en conteneur sur toute PR, depuis #578). Et Docker **ne change rien** au blocage réseau des sessions. **Remplacé par** un script d'installation versionné |
| 5 | **CPX21 — 3 vCPU / 4 Go / 80 Go** | aucun poste dimensionnant ne dépend du nombre d'apprenants : base 1,5 Mo, vidéos chez Vimeo, front statique, PHP-FPM réglé par `pm.max_children`. Et une machine **juste dimensionnée se surveille** — une machine 3× trop grande masque une fuite mémoire jusqu'au jour où elle ne la masque plus | ⛔ CPX31 (8 Go) : dimensionnait sur une croissance qui n'aura pas lieu sur ce LMS. ⛔ 2 Go : la bascule fait tourner `composer install` + migrations + restauration **pendant que la machine sert**. ⚠️ **Ne pas descendre le disque** : CPU/RAM se remontent en un redémarrage, l'agrandissement disque est **probablement définitif** — *non revérifié, `docs.hetzner.com` hors politique de sortie réseau* |
| 6 | **Restauration complète de la base le jour J** | à 1,5 Mo, une restauration prend quelques secondes et **se vérifie** | ⛔ le « merge des écarts » (question posée par le fondateur) : c'est précisément là qu'on perd des données |
| 7 | **MySQL 8.0 — ni 8.4, ni 9.x. PHP 8.2 d'abord** | changer de **moteur** et de **version** en même temps fait deux variables. PHP 8.2 est ce que la CI teste (`tests.yml:114`) | ⛔ prendre le plus récent « tant qu'on y est ». La montée en 8.4 est un chantier séparé (#471) |
| 8 | **`merge_train.sh` doit devenir inutile, pas être aménagé** | tant qu'il subsiste, **il constitue une voie parallèle à la CI** — exactement ce qu'on cherche à éviter | ⛔ **découper le script** en deux (proposition du pilote). **Objection du fondateur, et elle est juste** : « si c'est automatisé par la CI/CD, à quoi sert le script ? À déroger à la CI/CD ? Mais c'est ce qu'on veut éviter ». La bonne correction est de **protéger `main`** |
| 9 | **Modèle à trois branches** : `development` (travail) · `release/AAAA-MM` (figée, recette) · `main` (prod) | `development` sert aujourd'hui **à la fois** de branche de travail et de cible de recette : ce que les testeurs testent change sous eux | ⛔ continuer à recetter sur `development`. Le fondateur l'a formulé lui-même : « on n'a pas de copie stable de la version qui alimentera la prod parce qu'on en rajoute en permanence — et c'est moi qui le demande en grande partie » |
| 10 | **Le 4ᵉ environnement OVH reste sur OVH** | il ne concerne pas le LMS | ⛔ le migrer « tant qu'on y est » |
| 11 | **Une mesure se rejoue, elle ne se relit pas** | ⚠️ deux affirmations du CA de #301, mesurées le 11/08, étaient **fausses** au 22/08 : « FRONT : aucun conflit » → **15 conflits** ; « 15 bumps Dependabot sur `main` » → **1**. Onze jours suffisent | ⛔ citer une mesure datée comme si elle était courante |
| 12 | **Le blocage d'accès n'est pas un problème d'outil** | c'est une garde d'organisation Anthropic (§4.1) | ⛔ « installer `gh` » — proposé par le pilote, **retiré après mesure**. Le connecteur GitHub suffit pour merger ; le remède réel est de connecter l'application GitHub pour l'organisation |

---

## 3. Ce qui attend un arbitrage humain

⚠️ **Aucun de ces points n'est tranché. Aucun ne doit l'être par une session.**

### Posé au fondateur, sans réponse à ce jour

1. **Rattacher au jalon L5 : #600, #617, #618, #619, #623.** Aucune n'a de jalon.
   ⚠️ **Correction à ce que le pilote a annoncé** : il avait cité quatre orphelines
   (#617, #618, #619, #623) — **#600 l'est aussi**. Vérifié le 23/08.
   ⚠️ Et pour #618 : le jalon la rend **visible**, il ne la rend pas **urgente**. Elle l'est
   déjà, indépendamment de tout jalon.
2. **Supprimer la branche périmée `docs/181-runbook-bascule-prod`** (son contenu est mergé,
   en version dépassée — voir §4.4).
3. **Créer l'issue de migration serveur** — le chantier n'en a aucune.

### Arbitrages produit et technique ouverts

| sujet | ce qui bloque |
|---|---|
| **#141 · #142 · #143 · #144** — schéma et données visibles par les apprenants | **à trancher sur pièces**, après avoir joué les migrations sur le nouveau serveur et **regardé le résultat réel**. C'est le gain invisible du nouveau plan : ne plus trancher à l'aveugle |
| **#307** — qui déploie la production : le runbook ou la CI | survit au changement de serveur |
| **Exploitation courante après bascule** | ⚠️ **trou identifié, personne désignée.** Sauvegardes vérifiées, renouvellement TLS, supervision, astreinte. L'équipe externe ne gère pas l'infra |
| **#467** — Pint : arbitrage A/B/C | la dette de style legacy n'est pas une régression du merge |
| **#575 vs #626** — automatiser l'exécution des migrations, ou leur alerte ? | contradiction relevée par un autre fil, **non tranchée** |
| **Front, lot N5** — convergence `react-multi-select-component` → `react-select` | ⚠️ **règle 14** : la recherche dans les listes déroulantes est une capacité **nommément protégée**. L'équivalence de capacités n'est **pas** établie. N1→N4 sont mécaniques ; **N5 ne l'est pas** |
| **« Édition d'utilisateur par le directeur »** — capacité disparue | voulu, ou oubli ? Règle 14 : en cas de doute, demander |
| **59 vulnérabilités GitHub sur la branche par défaut** (1 critique, 22 hautes) | aucune instruction donnée |
| **Nommage de branche** | le pilote est contraint à `claude/efektivacademie-prod-deployment-g778z2`, alors que la charte impose **1 issue = 1 branche**. Non résolu |

---

## 4. Ce qu'une session neuve ne peut pas deviner

### 4.1 Le fait structurant : aucune session n'atteint les serveurs ni l'API GitHub

⚠️ **À lire avant de promettre quoi que ce soit.**

| blocage | mesure | conséquence |
|---|---|---|
| **SSH / HTTPS vers les serveurs** | `exit=56` sur `api.efektiv-academie.com` — **y compris sur le staging dont on sait qu'il répond 404** | un résultat vide depuis une session **ne prouve rien**. Tout geste serveur passe par un humain |
| **`api.github.com`** | **403** — « GitHub access is not enabled for this session. An org admin must connect the Claude GitHub App for this organization » | ni le proxy (`recentRelayFailures` vide), ni GitHub, ni une question d'IP : **une garde d'organisation**, active même sans jeton |
| **`docs.hetzner.com`** | bloqué par le proxy de sortie | le caractère irréversible du disque n'a pas pu être vérifié (§2, décision 5) |

**Ce qui marche quand même** : le **connecteur GitHub MCP** — lecture, écriture, PR, merge.
C'est par lui que tout a été fait. `gh` en ligne de commande, non.

⚠️ **Conséquence pratique** : le pilote **prépare, vérifie et corrige** ; il **n'exécute pas**.
Ne pas écrire de procédure qui suppose l'inverse.

### 4.2 (a) La structuration du dépôt git — matière pour la conversation dédiée

> ⚠️ Ce sujet **ouvre ailleurs**. Ce qui suit est la matière établie, écrite pour être
> reprise **par écrit** plutôt que de mémoire. Rien ici n'est une décision de ce document.

**Fait n°1 — la protection existe sur les QUATRE branches, à l'identique. Il n'y a aucune
asymétrie, et le vrai trou est le lint.**

> ⛔ **Ce fait a été corrigé le 23/08 au soir.** Sa première version affirmait « la branche de
> TRAVAIL est protégée, la branche de PRODUCTION ne l'est pas ». **C'était faux.** La
> correction est laissée visible parce que l'erreur de raisonnement vaut plus que le fait.

**Réglages lus par l'API** — conversation « structure du dépôt », jeton `AAZTEKDEV`, 23/08 :
⚠️ **mesure d'un autre fil, que cette session ne peut pas reproduire** (§4.1).

| | contrôle requis | `enforce_admins` | force-push | suppression |
|---|---|---|---|---|
| BACK `main` | `Suite — vert ou rouge (MySQL 8)` | **true** | non | non |
| BACK `development` | `Suite — vert ou rouge (MySQL 8)` | **true** | non | non |
| FRONT `main` | `tests` | **true** | non | non |
| FRONT `development` | `tests` | **true** | non | non |

Aucun ruleset sur l'un ou l'autre dépôt (`/rulesets` → `[]`).

**Pourquoi les deux merges du 23/08 se sont comportés différemment** — ce n'est pas la
branche qui diffère, c'est **quel contrôle était rouge** :

| PR | cible | `Suite (MySQL 8)` — **requis** | `Pint` — **non requis** | résultat |
|---|---|---|---|---|
| #621 | `main` | ✅ **success** | ❌ failure | merge accepté — **cohérent** |
| #636 | `development` | ⏳ *in progress* | ✅ success | merge refusé — **cohérent** |

⚠️ **La faute de raisonnement, nommée** : « le merge est passé » est un indice qui **vaut dans
les deux états** — « pas de protection » *et* « le seul contrôle requis était vert ». On ne
peut rien en conclure. Et **la preuve était dans le runbook lui-même, au §0.2** : « suite
MySQL 8 verte — 1645 tests / 4997 assertions » pour #621. J'ai écrit la conclusion inverse
230 lignes plus bas sans rouvrir mon propre texte.

✅ **Ce qui reste vrai** : la prémisse de `DEPLOIEMENT_STAGING.md` est bien **fausse** — la
protection n'est pas « indisponible sur le plan gratuit », elle est posée et active sur
quatre branches. **D4 est à réexaminer.**

⚠️ **Et voici le vrai trou : le lint n'est requis NULLE PART.** Un `Pint` rouge (BACK) ou un
`ESLint` rouge (FRONT) passe sur **les quatre branches**, `main` comprise. **#621 en est la
preuve vivante** : elle est entrée sur `main` avec Pint rouge, et c'était **régulier**.
L'arbitrage « le lint doit-il être bloquant, et où ? » rejoint **#467**.

**Fait n°2 — `merge_train.sh` est inexécutable depuis une session**, parce qu'il fait
`gh pr merge`. Et avec lui tombent ses deux gardes — alors que **la plus importante ne dépend
pas de `gh`** : « chaque commit de la PR est-il contenu dans la cible ? » se rejoue en git
pur (`git merge-base --is-ancestor`). C'est ce qui a été fait le 23/08, **plus** un contrôle
d'**identité d'arbre** que le script ne fait pas et qui est strictement plus fort.
**Dérogation tracée** : le merge du 23/08 n'est pas passé par le script (le CA de #301
l'imposait) mais par le connecteur.

**Fait n°3 — Dependabot ne peut pas être recablé.** Les mises à jour **de sécurité** ciblent
**toujours** la branche par défaut et **ne sont pas reciblables** par `dependabot.yml`.
⚠️ **C'est la cause d'une erreur réelle** : le pilote a fermé #296, #297, #298 et #299 en les
décrivant comme des mises à jour de version « qui reviendront par `development` ». **Faux.**
#296 portait CVE-2026-48784 et CVE-2026-45065 ; #297 six avis GHSA pour dompdf. **Impossible
de les rouvrir** : les branches sont supprimées à la fermeture. Écart réel mesuré après coup :
un seul, **dompdf 3.1.5 → 3.1.6** → issue **#623**. PR Dependabot encore ouvertes vers `main` :
#635, #634, #633, #632 (+ #329, configuration).

**Fait n°4 — le modèle de branches cible** et son unique point de bascule technique :
`deploy-staging.yml` se déclenche sur `push: branches: [development]` ; il doit se déclencher
sur `push: branches: ['release/**']`. **Une ligne dans chacun des deux dépôts.**
Deux règles sans lesquelles ça ne sert à rien :
1. ⚠️ un correctif de recette va sur `release/`, **PUIS** est reporté dans `development` —
   jamais l'inverse, jamais une seule des deux ;
2. ⚠️ à partir de la coupure, un merge dans `development` **n'arrive plus sur staging** —
   à annoncer à l'équipe **le jour même**.

**Impact sur la recette en cours : nul, à une condition** — couper `release/2026-09` depuis
`development` **à son état courant**, et **avant** de basculer le workflow. Pas l'inverse.

**Fait n°5 — une PR peut être bloquée pour toujours** : #570 porte l'ancien nom de contrôle
« Suite — vert ou rouge » alors que la protection exige « Suite — vert ou rouge (MySQL 8) ».
Le contrôle requis **ne s'exécutera jamais** sur cette branche. #572 la supersède.
⚠️ Renommer un job de CI condamne les PR ouvertes avant le renommage. C'est la classe de
problème que suit **#608**.

**Fait n°6 — `.github/dependabot.yml` est absent des DEUX branches.** La configuration vit
encore dans la PR **#329** (base `main`), toujours ouverte, qui prévoit de rediriger les
mises à jour **groupées** vers `development`. ⚠️ Tant que #329 n'est pas traitée, le même
écart se reformera **à chaque merge de bascule** — et rien ne re-proposera un bump fermé.

### 4.3 (b) L'état réel de la bascule

**La machine** : ⛔ **non commandée.** Hetzner, UE (Falkenstein / Nuremberg / Helsinki) —
données personnelles d'apprenants. Nom d'hôte `lms-prod` : il y en aura d'autres.

**Le dimensionnement** : **CPX21 (3 vCPU / 4 Go / 80 Go)**, §2 décision 5.
⚠️ **Le volume disque de la production n'est toujours pas mesuré** — c'est le seul chiffre
qui peut encore changer la réponse. Au-delà de ~40 Go : cran au-dessus.

**MyISAM → InnoDB (#136)** : jalon L5, `statut: en dev`, P0, bloquant.
- Constat au 11/08, re-mesuré : **MyISAM 58/58 tables, 0 FK**, `MAX(batch) = 5`, 62 lignes
  dans `migrations`.
- ⚠️ **Correction majeure à connaître** : la base de production est **`e-learning-prod`**,
  **pas `elearning`** — cette dernière est une copie **figée au 12/02**. Le constat
  « 55 tables MyISAM » des docs 07 et 11 porte sur **la mauvaise base**.
- **Ce que le nouveau plan change** : la conversion ne se fait plus **en place**. On restaure
  le dump dans une base MySQL 8 **neuve, déjà InnoDB** ; les cascades s'activent sur une copie
  qu'on inspecte **avant** qu'elle soit vivante. Le blocant B2 disparaît **par la forme**,
  pas par un correctif.
- ⛔ **Reste dû** : le script de conversion **n'est ni versionné ni testé** (B14, livrable de
  #156). Critère de sortie : **rejouer la séquence complète deux fois d'affilée sans
  intervention manuelle**.

**Rattrapage de migrations (#137)** : jalon L5, `statut: en recette`.
- Cause : le recalage du 06/08 a inséré des noms de migrations dans la table `migrations`
  **sans vérifier objet par objet** que chaque colonne existait.
- Diff exhaustif fait le 08/08 (PR #145 rapport + outillage `scripts/schema/`, PR #146
  migrations idempotentes). Résultat : sur **17 colonnes manquantes**, **une seule** ne se
  comblera jamais seule — **`courses.category_id`** ; les 16 autres et 6 tables relèvent de
  migrations non enregistrées, donc de **retard de déploiement, pas de dette**. S'y ajoutent
  **11 divergences silencieuses** (type, nullabilité, défaut) qu'une comparaison de **noms**
  ne pouvait pas voir.
- ⚠️ **`courses.category_id` préexiste au recalage** (enregistrée en batch 1, depuis
  l'installation d'origine). Le recalage l'a **aggravée et masquée**, il ne l'a pas créée.
- ⚠️ **Deux règles à ne pas perdre** : ⛔ **ne pas recaler** la table `migrations` avant la
  preuve objet par objet ; ⚠️ **« Nothing to migrate » ne prouve rien** — la commande ne lit
  que la table `migrations`. Seul `scripts/schema/diff_schema.py` fait preuve.

**Reprise des accès** :
- Répartition arrêtée le 23/08 : **comptes serveur, SSH, exécution → Enguerran et son
  associé** ; **DNS (reste chez OVH) → Enguerran** ; **scripts, procédures, contrôles, git,
  PR, issues → le pilote** ; **exploitation courante après bascule → à définir, trou ouvert**.
- ⚠️ **L'équipe de développement externe ne gère pas l'infra** (constat du fondateur). La
  reprise n'est **pas seulement un transfert d'accès : il n'y a personne derrière.**
- ⛔ **Le `.htaccess` du front n'est versionné dans aucun dépôt** et `deploy-staging.yml`
  l'exclut **deux fois** du `rsync --delete`. **Le seul exemplaire existant est sur le
  serveur OVH.** Sans lui, tous les liens profonds de la SPA tombent en 404 sur une machine
  neuve — c'est ce qui a fait tomber le staging le 09/08 (#176, blocant **B11**).

**🔴 #618 — l'incident ouvert, confirmé EN PRODUCTION le 23/08 à 05:50**
- Le disque `storage/app/public` **est servi publiquement**, sous
  `/e-learning-api/public/storage/`. Les répertoires répondent **403** (listage coupé) mais
  **les fichiers sortent en 200, sans aucune authentification**.
- Preuve faite **délibérément sur une image de cours** pour ne toucher à aucune donnée
  personnelle. ⚠️ **Aucun certificat n'a été téléchargé.**
- Ampleur : **certificates 150** · avatars 82 · course-image 92 · course_resources 40 ·
  lesson-file 4. ⚠️ **Les noms sont devinables** — forme réelle `certificate_104_35.pdf` :
  **deux entiers petits et séquentiels suffisent à énumérer l'ensemble.**
- ⚠️ **L'ordre compte, et il est contre-intuitif** : (1) couper l'accès à `certificates/`
  **seulement** — couper tout `storage/` casse l'affichage des avatars et des images de
  cours ; (2) **lire les journaux d'accès AVANT de corriger** — la correction efface la
  trace, et c'est la seule façon de savoir s'il faut notifier qui que ce soit ; (3) **ensuite
  seulement** servir par une route authentifiée.
- ⚠️ **Le doute levé sur staging ne tenait pas pour la production** : sur staging, les mêmes
  chemins répondaient 404. Conclure de l'un sur l'autre aurait fermé le sujet à tort.

### 4.4 Branches, worktrees, PR en vol

| dépôt | clone local | branche |
|---|---|---|
| `EFEKTIVACADEMIE-BACK` | `/home/user/EFEKTIVACADEMIE-BACK` | `claude/efektivacademie-prod-deployment-g778z2` |
| `EFEKTIVACADEMIE-FRONT` | `/home/user/EFEKTIVACADEMIE-FRONT` | `main` |
| `MICROLEARNING` | `/home/user/MICROLEARNING` | `claude/efektivacademie-prod-deployment-g778z2` |

**Aucun worktree secondaire ouvert par cette session** (`git worktree list` → une entrée par dépôt).

⚠️ **La branche du pilote a été redémarrée depuis `origin/development`** après le merge de
#639 : une PR mergée ne se réutilise pas.

⚠️ **Piège vécu, à ne pas rejouer** : un `git checkout origin/development -- .` lancé dans le
clone FRONT y a laissé **466 fichiers indexés**. Le nettoyage n'a pas défait l'index, et le
pilote a **affiché « (propre si vide) » sur une sortie non vide**. Un autre fil l'a détecté,
a sauvegardé un patch, a nettoyé et a travaillé en worktree dédié.
**Vérifier `git status` du clone FRONT avant d'y toucher.**

**PR ouvertes sur BACK au 23/08** — 12 :
`#638` consignation recette · `#629` cahier v1.6 · `#620` corrections état-avant-prod ·
`#583` arbitrages du 21/08 · `#572` écarts infra cloud · `#570` cadrage infra cloud
(⛔ **bloquée pour toujours**, §4.2 fait n°5) · `#455` pipeline ·
`#635` `#634` `#633` `#632` Dependabot → `main` · `#329` configuration Dependabot → `main`.

⚠️ **Branche périmée** : `docs/181-runbook-bascule-prod` — son contenu est mergé, en version
**dépassée**. La PR **#306**, qui aurait **écrasé la révision du 23/08 par la version du
11/08**, a été trouvée et fermée. **La branche, elle, existe encore.**

### 4.5 Secrets — par NOM de variable uniquement

⚠️ **Aucune valeur de secret n'a été lue, ni écrite, ni transmise dans cette session.**
Les noms seuls, pour qu'une session neuve sache **quoi demander** sans avoir à fouiller :

- **Secrets GitHub Actions (BACK)** : `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_SSH_KEY`.
- **`.env` applicatif** : `APP_KEY`, `APP_ENV`, `APP_DEBUG`, `APP_URL`, `FRONT_URL`,
  `DB_CONNECTION`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `CACHE_STORE`,
  `SESSION_DRIVER`, `QUEUE_CONNECTION`, `FILESYSTEM_DISK`, `SANCTUM_STATEFUL_DOMAINS`,
  `CORS_ALLOWED_ORIGINS`, `MAIL_MAILER`, `MAIL_SCHEME`, `MAIL_ENCRYPTION`,
  `MAIL_FROM_ADDRESS`, `MAIL_FROM_NAME`, `PUSHER_APP_ID`, `PUSHER_APP_CLUSTER`,
  **`STRIPE_KEY`** (⚠️ sa nature — test ou production — est **la question ouverte de #600**).
- **Front (Vite)** : `VITE_APP_API`, `VITE_APP_MEDIA_URL`, `VITE_APP_PROFILE_URL`,
  `VITE_APP_PUSHER_APP_KEY`, `VITE_APP_PUSHER_APP_CLUSTER`.
- **#619** : le **DSN Sentry n'est posé sur aucun environnement** — le serveur ne signale rien.

⚠️ **Le `.env` de production n'est PAS versionné et n'existe nulle part ailleurs que sur le
serveur OVH.** Comme le `.htaccess`, il se récupère dans une session SSH humaine.

### 4.6 Tâches planifiées, abonnements, pages publiées

- **Aucune tâche planifiée** créée par cette session (ni `send_later`, ni Routine, ni cron).
- **Aucun artefact ni page publiée.** Rien à mettre à jour plutôt qu'à recréer.
- **Abonnements PR** : celui de #639 s'est **fermé automatiquement au merge**. Aucun abonnement
  actif à la clôture.
- ⚠️ Un livrable **n'est pas** dans le dépôt : l'**inventaire du front** (341 fichiers, 99
  écrans, 18 types d'objets, les 3 doublons nommés, les 9 paquets morts, le double import
  Bootstrap et sa feuille de neutralisation, le découpage N1→N6). Il n'existe que dans le fil
  de conversation. **C'est une dette de cette session, et exactement le défaut que la leçon du
  22/08 décrit.**

### 4.7 Les erreurs de cette session — pour qu'elles ne se rejouent pas

| erreur | ce qu'elle a coûté | la règle qui en sort |
|---|---|---|
| **4 PR Dependabot de sécurité fermées à tort** (#296-#299) | irréversible — branches supprimées à la fermeture | ⛔ **une mise à jour de sécurité ne se ferme pas** au motif qu'elle « reviendra par `development` » : elle cible la branche par défaut et n'est pas reciblable |
| **Clone FRONT pollué**, 466 fichiers laissés indexés | un autre fil a dû sauver un patch et nettoyer | ⛔ ne jamais afficher un commentaire rassurant sur une sortie qu'on n'a pas lue |
| **« Il n'y a pas de protection de branche sur ce dépôt »** | affirmation reprise de `DEPLOIEMENT_STAGING.md`, **falsifiée le jour même** par deux merges | ⛔ ne pas reprendre la prémisse d'un document comme un fait mesuré |
| **Issue #623 créée en double de #622** (même écart dompdf, deux fils en parallèle) | #622 fermée, son contenu transplanté | ⛔ `scripts/chercher_issue.sh` ne protège pas d'un **fil concurrent** — annoncer une création avant de la faire |
| **Quatre orphelines annoncées, il y en a cinq** (#600 oubliée) | corrigé en §3 | ⛔ un décompte s'établit par requête, pas de mémoire |
| **« `main` n'est pas protégée »** — conclu de deux merges, **faux** | un autre fil a dû lire l'API pour me corriger ; j'ai failli faire « réparer » une asymétrie inexistante | ⛔ **un indice qui vaut dans les deux états ne conclut rien.** « Le merge est passé » s'explique aussi bien par « pas de protection » que par « le seul contrôle requis était vert ». ⚠️ Et la réfutation était **dans mon propre document**, au §0.2 du runbook, 230 lignes plus haut |
| **« 260 apprenants, 180 inscriptions »** dans le runbook §0.4 | deux chiffres faux dans un document de référence | ⛔ le bon chiffre était **dans le même fichier**, au §1.4 : 301 utilisateurs, 2 128 inscriptions. Un chiffre se **relève**, il ne s'estime pas |

---

## 5. Les mesures DÉJÀ FAITES sur les serveurs — commande, sortie, date

> ⚠️ **Pourquoi cette section existe** : une mesure non écrite sera refaite sur la
> production. Chaque ligne ci-dessous est une commande qui n'a **pas** à être relancée
> pour retrouver son résultat.
>
> ⛔ **Mais une mesure périme.** La plus récente ici date du 11/08 pour tout ce qui est
> SSH — **douze jours**. Une mesure du 11/08 relue le 22/08 s'est déjà révélée fausse
> (§2, décision 11). **Relancer pour agir, pas pour savoir.**

### 5.1 ⚠️ D'abord : qui a mesuré quoi, et depuis où

Ce point conditionne la lecture de tout le reste, et il porte une **contradiction que je
lève ici plutôt que de la recopier**.

| session | peut atteindre les serveurs ? | preuve |
|---|---|---|
| **La session SSH du 08 et du 11/08** | **oui** — un humain avec `ubuntu@57.129.1.122` | les relevés §5.2 et §5.3 existent |
| **La session du 22/08** | **non** | `exit=56` partout, **y compris sur le staging dont on sait qu'il répond 404** |
| **Le fil du 23/08 à 05:50** | **oui, en HTTPS** | codes 403 et 200 rapportés dans #618 |
| **CETTE session, 23/08 au soir** | **NON** | re-mesuré, ci-dessous |

**Re-mesure faite maintenant, en lecture seule, sur des chemins sans donnée personnelle :**

```bash
for u in "https://api.efektiv-academie.com/e-learning-api/public/storage/course-image/" \
         "https://api.efektiv-academie.com/e-learning-api/public/storage/certificates/" \
         "https://staging.efektiv-academie-dev.com/storage/certificates/"; do
  code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$u"); echo "code=$code exit=$? $u"
done
getent hosts api.efektiv-academie.com staging.efektiv-academie-dev.com
```

```
code=000 exit=56  …/public/storage/course-image/
code=000 exit=56  …/public/storage/certificates/
code=000 exit=56  https://staging.efektiv-academie-dev.com/storage/certificates/
57.129.1.122    api.efektiv-academie.com
57.129.1.122    staging.efektiv-academie-dev.com
```

⚠️ **Ce que ça établit, et ce que ça n'établit pas.** La résolution DNS **est** une mesure
de cette session : **production et staging sont bien sur la même machine**. Mais les trois
sondes HTTP échouent au niveau du proxy sortant — **je ne peux pas reproduire les codes 403
et 200 rapportés le 23/08 à 05:50**, ni les décomptes de fichiers (150 certificats, 82
avatars…) qui figurent dans #618.

⛔ **Je ne les remets pas en doute — je dis d'où ils viennent.** Ils ont été produits par un
autre fil, sur mandat, et **c'est ce fil qui porte la preuve**. Un décompte par répertoire ne
s'obtient pas d'un `403` : il suppose un accès que cette session n'a pas. **À faire préciser
avant d'en tirer une notification RGPD.** La conclusion de #618, elle, ne dépend pas du
décompte : un seul certificat servi en 200 sans authentification suffit.

### 5.2 Inventaire de la production par SSH — 11/08, lecture seule

`ssh ubuntu@57.129.1.122` · **aucune écriture, aucun redémarrage, aucune fenêtre de
maintenance.** Huit familles de commandes, reproduites dans le runbook §1.0.

| # | commande | ce qu'elle a donné |
|---|---|---|
| 1 | `git remote -v` · `git branch --show-current` · `git log -1` · `git status --porcelain` | remote = **`https://github.com/Rigictech/e-learning-api.git`** (dépôt de l'ancienne équipe), branche `main`, dernier commit **`166a8b2b` du 01/08/2025**, **111 fichiers suivis modifiés**, 116 non suivis, 7 branches locales `rupak-*` |
| 2 | `ls -la` racine Laravel + vhost parent | copies manuelles hors git : `app--25-02-2026/`, `app-08-05-2026/`, `app-old-13-11/`, `app-old-16-10/`, `config-07-11-2025/`, `.env.backup-avant-debug-20260806-102004`, `routes/api.php-old`, `routes/api.php-25-02-2026`, 2 `Requests/*` datés |
| 3 | `find … -exec sha256sum` sur **294 fichiers** | vs `origin/main` : **145 contenus différents**, 138 identiques, 11 en prod seulement · vs `origin/development` : 152 différents, 131 identiques |
| 4 | `git hash-object` sur les 294 | **283 des 294 contenus existent déjà dans notre historique.** Meilleure correspondance d'une référence git avec le déployé : **`origin/main` = 148/287, soit 51 %** |
| 5 | `grep` nominatif sur `.env` — **valeurs masquées** | `FILESYSTEM_DISK=local`, et les noms listés en §4.5. ⚠️ **Aucune valeur de secret n'a été lue ni consignée** |
| 6 | `SELECT` / `information_schema` sur `e-learning-prod` | voir §5.3 |
| 7 | `cat` du `.htaccess` front · `ls /etc/apache2/mods-enabled/` | racine servie `/var/www/efektiv-academie.com`, bundle `index-ivHaxGJI.js` déposé le **01/07/2026** · `.htaccess` : **rewrite SPA présent, AUCUNE règle de cache** · `mod_headers`, `mod_rewrite`, `mod_deflate` **chargés** |
| 8 | `ls ~/.ssh` · `sudo -n true` · `df -h` · `php -v` · `composer --version` | **Composer 2.2.6 (2022)** — c'est #303 |

⚠️ **La sortie de `df -h` n'a pas été consignée**, et c'est précisément le chiffre qui
manque aujourd'hui pour figer le gabarit (§0.7 étape 0 du runbook). **C'est l'exemple même
de ce que cette section veut empêcher** : la commande a été lancée le 11/08, sa sortie n'a
pas été écrite, il faut la relancer.

⚠️ **Le `cat` du `.htaccess` a été fait, mais son CONTENU n'a pas été versionné.** Le fichier
n'existe dans aucun dépôt (#176, blocant B11). **La commande la plus importante de la liste
est donc à refaire** — non pour mesurer, pour **récupérer**.

### 5.3 La base de production — 11/08, lecture seule

Relevé sur **`e-learning-prod`** — ⚠️ **pas `elearning`**, qui est une copie **figée au
12/02**. **Identique en tous points au relevé du 08/08** (`19_DIFF_SCHEMA_PROD.md`).

| mesure | valeur |
|---|---|
| MySQL | **`8.0.46-0ubuntu0.22.04.3`** |
| Base | **1,46 Mo**, **58 tables**, 442 colonnes |
| Moteur | **MyISAM 58 / 58** |
| Clés étrangères | **0** — attendu : **64** |
| Table `migrations` | **62 lignes**, `MAX(batch) = 5` (le recalage fautif du 06/08) |
| Volumétrie | **301 utilisateurs · 54 cours · 2 128 inscriptions · 135 complétions · 313 leçons** |

**Objets contrôlés un par un** — `courses.category_id` **absente** alors que sa migration est
enregistrée en batch 1 (→ SQL 1054 → 500 **aujourd'hui, en production**, #141) · 5 colonnes
absentes dont les migrations ne sont **pas** enregistrées (se comblent au prochain `migrate`) ·
**6 tables absentes** (`login_events`, `lesson_views`, `quiz_attempts`, `media_progress`,
`resource_downloads`, `support_types`) — ce sont elles qui déclenchent l'errno 1824 · et
`courses.order_no` / `users.is_active` **présentes** avec migrations non enregistrées, piège
errno 1060 désormais gardé par `hasColumn`.

**Références pendouillantes** (errno 1452, prérequis de #136) : `subcategories → categories` **1** ·
`lessons → sections` **89** · `lessons → courses` **16** (⚠️ **inclus dans les 89**, purgés par
ricochet — *levé par la mesure, pas par raisonnement*) · `users → users(parent_id)` **5** ·
`enrollments → courses` et `→ users` **0 / 0**.

⚠️ **À reconfirmer lors de la répétition #156** : si l'ordre des migrations plaçait une
création de FK **avant** la purge, les 16 redeviendraient bloquants.

### 5.4 ⛔ Ce qui n'a JAMAIS été mesuré — et qui bloque

| mesure manquante | pourquoi ça bloque | à faire dans la MÊME session SSH |
|---|---|---|
| **Volume disque** (`du -sh` par répertoire de `storage/app/public`, `df -h /`) | fige le gabarit — au-delà de ~40 Go, cran au-dessus | ✅ |
| **Contenu du `.htaccess` du front** | sans lui, tous les liens profonds tombent en 404 sur la machine neuve | ✅ |
| **Journaux d'accès sur `/storage/certificates/`** | ⚠️ **seule façon de savoir s'il faut notifier qui que ce soit** — et **la correction efface la trace** | ✅ **avant** tout correctif |
| **Nature de la clé `STRIPE_KEY`** (test ou production) | c'est #600, ouverte depuis le 22/08 | ✅ |
| **`.env` de production** | non versionné, n'existe nulle part ailleurs | ✅ |

⚠️ **Ces cinq mesures tiennent dans une seule connexion.** Les grouper est le seul moyen de
ne pas revenir cinq fois sur une machine de production.
