# Rapport final — Identité visuelle, page ATL, mouvement « On ne lâche rien ! »

Rapport rédigé le 22 juillet 2026, à l'issue d'une session interrompue une fois par une coupure de
connexion puis reprise et auditée intégralement avant complément.

---

## 1. Informations ATL trouvées et sources

Voir le document dédié [docs/recherches-atl.md](recherches-atl.md), qui reste la référence à jour
et n'a pas eu besoin d'être complété lors de la reprise. Résumé :

- La page Facebook `https://www.facebook.com/AmisdeTengeLitho` **bloque l'accès automatisé** — un
  seul fragment de titre a pu être récupéré, aucun contenu exploitable (publications, événements,
  nombre d'abonnés). Ce blocage est documenté explicitement, sans contournement ni invention.
- Éléments **confirmés** (recoupés avec le site officiel `telithotenge.cd`) : nom complet du réseau
  « ATL — Les Amis de Tenge Litho », existence d'une section de navigation dédiée sur le site
  officiel (Activités / Adhésion / Contact — actuellement sans contenu propre, toutes ces
  sous-pages pointent vers `index.php`), lien Facebook officiel `https://fb.me/AmisdeTengeLitho`,
  devise « On ne lâche rien ! » reprise sous la forme « Les amis de Tenge Litho, on ne lâchera
  rien ».
- Éléments **non vérifiés**, et donc jamais affirmés sur le site : statut juridique d'ATL, date ou
  contexte de création, contenu réel des activités propres à ATL (par opposition à celles de Te
  Litho lui-même), nombre de membres.
- **Aucune image Facebook n'a été importée** : la galerie de la page `/atl` s'appuie uniquement sur
  des photos déjà présentes dans la médiathèque du projet (album dédié, voir §9).

## 2. Pages et routes disponibles

| Route | Contrôleur | Fonction |
|---|---|---|
| `GET /atl` | `AtlController@index` | Page éditoriale ATL |
| `GET /rejoindre` | `MovementMemberController@create` | Formulaire de suivi du mouvement |
| `POST /rejoindre` | `MovementMemberController@store` | Traitement de l'inscription |
| `GET /rejoindre/confirmer/{token}` | `MovementMemberController@confirmEmail` | Confirmation d'adresse e-mail |
| `GET /rejoindre/desinscription/{token}` | `MovementMemberController@unsubscribe` | Désinscription |
| `GET /admin/movement-members` | `Admin\MovementMemberAdminController@index` | Liste + tableau de bord |
| `GET /admin/movement-members/export` | `@export` | Export CSV |
| `GET /admin/movement-members/{id}` | `@show` | Fiche détaillée |
| `PUT /admin/movement-members/{id}` | `@update` | Statut général + notes |
| `POST /admin/movement-members/{id}/atl-status` | `@updateAtlStatus` | Statut ATL |
| `DELETE /admin/movement-members/{id}` | `@destroy` | Suppression logique |
| `POST /admin/movement-members/{id}/restore` | `@restore` | Restauration |
| `GET /admin/messages` | `Admin\ContactMessageAdminController@index` | Liste des messages de contact |
| `GET /admin/messages/{id}` | `@show` | Détail (marque lu) |
| `POST /admin/messages/{id}/status` | `@updateStatus` | Changement de statut |
| `POST /admin/messages/{id}/resend` | `@resend` | Renvoi manuel de la notification |
| `DELETE /admin/messages/{id}` | `@destroy` | Suppression logique |
| `GET/POST /admin/mail-test` | `Admin\MailTestController` | Test d'envoi, **super-admin uniquement** |

Toutes ces routes ont été vérifiées via `php artisan route:list` : aucun doublon, aucun conflit
d'ordre avec les routes existantes de la médiathèque/chronologie.

Navigation publique : lien « ATL » ajouté au menu principal (`partials/nav.blade.php`), lien ATL et
lien Facebook ajoutés au pied de page. La page `/mouvement` a été conservée telle quelle et propose
un double appel à l'action (« Rejoindre le mouvement » / « Découvrir ATL ») plutôt que d'être
fusionnée avec `/atl`, pour éviter une duplication de contenu.

## 3. Identité visuelle

- **Logo principal** (`images/onnelacherien.jpeg`, original non modifié) : variantes générées en
  64/96/160/320px, PNG + WebP, dans `public/images/brand/`. Utilisé dans le header, le menu mobile,
  le footer et l'espace admin (petit format à côté du titre « Administration »). Texte alternatif
  renseigné partout.
- **Favicon** (`images/on_ne_lache_rien.png`, original non modifié) : `favicon.ico` (multi-résolution
  16/32/48, format PNG-in-ICO), `favicon-16x16.png`, `favicon-32x32.png`, `apple-touch-icon.png`
  (fond opaque bleu foncé pour éviter la transparence sur iOS), `android-chrome-192x192.png`,
  `android-chrome-512x512.png`, `site.webmanifest`. Toutes les balises `<head>` nécessaires ont été
  ajoutées dans `components/layout.blade.php` (icon, apple-touch-icon, manifest, theme-color).
  Vérifié servi en HTTP 200 sur le domaine réel `tlt.test`.
- **Logo ATL** (`images/ATL.png`, original non modifié) : variantes 160/320/640px (largeur,
  proportions conservées) en PNG + WebP. Présent sur la page `/atl` (hero), dans le formulaire
  d'adhésion (à côté de la case à cocher ATL — ajouté lors de la reprise, voir §12) et dans la
  section ATL de l'e-mail de bienvenue.
- Aucune image n'a été étirée, déformée ou recadrée de façon destructive ; les originaux dans
  `images/` sont restés strictement inchangés (vérifié par horodatage de fichier).

## 4. Base de données — tables et migrations

Deux migrations créées et exécutées (`php artisan migrate` — confirmé via `migrate:status`, batch 8) :

- `2026_07_22_101111_create_movement_members_table.php` — table `movement_members` : civility,
  full_name, country, city, commune, email (unique), whatsapp_number, whatsapp_normalized (indexé),
  atl_membership_requested, communication_consent, privacy_consent_at, source, status (enum),
  atl_status (enum), verification_token (unique), email_verified_at, unsubscribe_token (unique),
  ip_address, user_agent, welcome_email_sent_at, admin_notes, soft deletes. Index sur status,
  atl_status, country, city.
- `2026_07_22_101240_add_status_and_soft_deletes_to_contact_messages_table.php` — ajoute à
  `contact_messages` : status (enum nouveau/lu/traité/archivé), notification_sent_at,
  acknowledgement_sent_at, notification_error, user_agent, soft deletes.

Aucune SQL spécifique à SQLite n'a été utilisée (types Laravel portables : `enum()`, `softDeletes()`,
`timestamp()`, `unsignedBigInteger` implicite des clés). Les tables `jobs`, `job_batches`,
`failed_jobs` existaient déjà (squelette Laravel par défaut) — aucune migration supplémentaire
requise pour la file d'attente.

## 5. Modèles

- `App\Models\MovementMember` — helpers `normalizeEmail()`, `normalizeWhatsapp()`,
  `maskedEmail()`/`maskedWhatsapp()` (masquage partiel pour les listes admin), `civilityLabel()`,
  scope `atlRequested()`.
- `App\Models\ContactMessage` — `statusLabel()`, soft deletes.

**Bug corrigé lors de la reprise** : les deux modèles utilisent les attributs PHP `#[Fillable([...])]`
plutôt que la propriété classique `$fillable`. Deux champs avaient été omis de cette liste sur
`ContactMessage` (`notification_sent_at`, `acknowledgement_sent_at`) — l'assignation de masse les
ignorait silencieusement, si bien que les Jobs d'envoi tournaient sans erreur mais n'enregistraient
jamais la date d'envoi réel. Corrigé (voir §11).

## 6. E-mails

Trois Mailables, chacune avec une vue dédiée compatible clients de messagerie (mise en page par
tableaux HTML, styles inline) :

- `App\Mail\WelcomeMovementMail` → `emails.movement.welcome` — salutation personnalisée, présentation
  du mouvement, bouton de confirmation d'adresse, section ATL conditionnelle avec le texte exact
  demandé (« a bien été enregistrée [...] ne constitue pas encore une acceptation officielle comme
  membre »), liens site/ATL/contact/désinscription. **Ne dit jamais** « Vous êtes membre d'ATL ».
- `App\Mail\ContactNotificationMail` → `emails.contact.notification` — envoyée à
  `contact@telithotenge.com`, `replyTo` pointant vers le visiteur.
- `App\Mail\ContactAcknowledgementMail` → `emails.contact.acknowledgement` — accusé de réception
  envoyé au visiteur.

Chaque Mailable est enveloppée par un Job dédié (`SendMovementWelcomeEmail`,
`SendContactNotification`, `SendContactAcknowledgement`) qui, lui, implémente `ShouldQueue` et met
à jour `*_sent_at` ou `notification_error` selon le résultat.

**Bug d'architecture corrigé lors de la reprise (double mise en file)** : les trois classes
Mailable implémentaient elles-mêmes `ShouldQueue`, en plus des Jobs qui les enveloppent déjà. Or
`Illuminate\Mail\Mailer::sendMailable()` re-met en file un Mailable `ShouldQueue` au lieu de
l'envoyer, quel que soit le contexte d'appel. Résultat concret constaté en conditions réelles (voir
§13) : un `queue:work` traitait le Job, qui appelait `Mail::send()`, qui **re-mettait discrètement le
mail en file** au lieu de l'envoyer — il fallait deux passages de worker pour qu'un e-mail parte
réellement, et surtout la gestion d'erreur du Job (`notification_error`, `*_sent_at`) ne
s'exécutait jamais sur le second niveau. Corrigé en retirant `implements ShouldQueue` des trois
classes Mailable (elles restent de simples Mailables envoyés de façon synchrone **depuis
l'intérieur** d'un Job déjà mis en file — c'est le Job qui porte la responsabilité de la file
d'attente). Un seul passage de worker suffit désormais à livrer l'e-mail, vérifié en conditions
réelles après correction.

## 7. Configuration SMTP OVHcloud

`.env` (mot de passe jamais renseigné en clair, jamais dans le dépôt) :

```
MAIL_MAILER=log
MAIL_HOST=ssl0.ovh.net
MAIL_PORT=465
MAIL_USERNAME=contact@telithotenge.com
MAIL_PASSWORD=
MAIL_SCHEME=smtps
MAIL_FROM_ADDRESS=contact@telithotenge.com
MAIL_FROM_NAME="Te Litho Tenge Didier"
```

Point technique important : cette version de Laravel (config/mail.php) ne lit **plus**
`MAIL_ENCRYPTION` (clé absente de la configuration du mailer `smtp`) mais `MAIL_SCHEME`. Le port 465
d'OVHcloud est du TLS implicite dès la connexion : le schéma Symfony Mailer correspondant est
`smtps` — utiliser `ssl` comme le suggérait la demande initiale n'aurait rien fait (schéma non
reconnu), avec le risque d'une tentative de connexion en clair. Ce point a été vérifié et corrigé de
façon proactive.

Tant que `MAIL_PASSWORD` n'est pas renseigné, le mailer reste volontairement sur `log` : aucun envoi
n'est simulé comme réussi, les e-mails sont simplement écrits dans
`storage/logs/laravel.log` (contenu vérifié lisible et correctement formé). Un écran de test
`/admin/mail-test`, réservé aux super-administrateurs (vérifié : un administrateur simple reçoit
403, un super-administrateur accède à la page), affiche la configuration active (hôte, port, schéma,
utilisateur, mot de passe configuré ou non — **jamais sa valeur**) et permet un envoi de test. La
commande `php artisan mail:test {to}` fait de même en CLI.

## 8. Formulaire de contact

Le flux existant a été renforcé, pas réécrit : `ContactController@store` enregistre systématiquement
le message en base **avant** de distribuer les Jobs de notification/accusé de réception — une panne
SMTP ne fait donc jamais perdre le message. Statuts : nouveau / lu / traité / archivé. Dans
l'administration (`/admin/messages`) : consultation (marque automatiquement « lu »), changement de
statut, renvoi manuel de la notification en cas d'échec, suppression logique, restauration possible
en base (soft delete Eloquent standard).

## 9. Médiathèque et album ATL

Un album « ATL — Les Amis de Tenge Litho » a été ajouté au seeder existant et contient 69 images déjà
présentes dans la médiathèque (toutes publiées), sélectionnées pour leur compatibilité de contexte
avec ATL/le mouvement — aucune image n'a été importée depuis Facebook. La page `/atl` utilise la
couverture de l'album comme image de fond du hero et jusqu'à 12 images de la galerie en cartes
éditoriales, chacune administrable (ordre, légende, texte alternatif) depuis la médiathèque existante,
sans nouveau code d'administration spécifique à ATL — la médiathèque générale suffit.

## 10. Administration

`/admin/movement-members` : tableau de bord (total, confirmés, demandes ATL, membres ATL acceptés,
30 derniers jours, répartition pays/ville), recherche, filtres (pays, ville, commune, statut, statut
ATL, demande ATL uniquement, e-mail confirmé, corbeille), liste avec e-mail et WhatsApp masqués,
fiche détaillée (coordonnées complètes, non masquées, car nécessaires à l'usage opérationnel), statut
général et notes internes, statut ATL avec rappel explicite à l'écran (« une demande en ligne ne
constitue jamais une adhésion officiellement acceptée tant que le statut n'est pas passé à ACCEPTED
ici »), suppression logique et restauration, export CSV. `/admin/messages` : liste filtrée par
statut/corbeille, détail, changement de statut, renvoi manuel, suppression logique. Toutes ces
actions sont journalisées dans `media_activity_logs` (réutilisation du journal d'activité existant,
avec `subject_type` = `MovementMember` ou `ContactMessage`).

Liens de navigation ajoutés dans la barre latérale admin : « Membres du mouvement », « Demandes ATL »
(lien filtré), « Messages de contact », et « Test e-mail » (visible uniquement si
`is_super_admin`). Cartes de raccourci ajoutées au tableau de bord admin général.

## 11. Corrections apportées lors de la reprise après coupure

La coupure était survenue juste avant la rédaction de ce rapport — l'ensemble du code fonctionnel
(migrations, contrôleurs, vues, e-mails) était déjà en place et les 93 tests de l'époque passaient.
La reprise a consisté en un audit réel (fichiers, routes, DB, tests, puis un **smoke test complet sur
le domaine réel** `http://tlt.test`, pas seulement une relecture) plutôt qu'en une reprise aveugle du
développement. Cet audit a mis au jour deux bugs réels, invisibles dans la suite de tests d'alors
car masqués par `Mail::fake()`/`Queue::fake()` :

1. **Mass-assignment silencieuse** — `notification_sent_at` et `acknowledgement_sent_at` absents du
   `#[Fillable]` de `ContactMessage` (§5). Corrigé.
2. **Double mise en file des e-mails** — les Mailables implémentaient `ShouldQueue` en plus de leurs
   Jobs enveloppants (§6). Corrigé.
3. **Logo ATL absent du formulaire d'adhésion** malgré la demande explicite (« Le logo ATL doit
   apparaître [...] dans le formulaire d'adhésion ATL ») — ajouté à côté de la case à cocher
   correspondante dans `mouvement/rejoindre.blade.php`.
4. **Content-Type manquant sur l'export CSV** (`Response::streamDownload` ne déduisait pas
   `text/csv` automatiquement) — en-tête ajouté explicitement.

Ces quatre corrections ont été détectées et validées par un smoke test réel (pas seulement des
tests unitaires) : inscription réelle via HTTP sur `tlt.test`, traitement réel de la file d'attente
avec `php artisan queue:work`, connexion admin réelle avec un compte jetable créé puis supprimé,
changement de statut ATL réel, export CSV réel, confirmation d'e-mail et désinscription réelles via
leurs liens à jeton. Toutes les données de test créées pendant cet audit (messages, inscriptions,
compte administrateur jetable, jobs en file) ont été supprimées après vérification — aucune donnée
réelle n'a été touchée.

Trois tests de non-régression ont été ajoutés spécifiquement pour ces bugs, afin qu'ils ne puissent
pas resurgir silencieusement :
`test_notification_and_acknowledgement_jobs_actually_persist_their_sent_timestamps`,
`test_welcome_email_job_actually_persists_its_sent_timestamp` (exécutent réellement les Jobs, sans
mock, pour vérifier la persistance) et `test_mailable_classes_do_not_implement_should_queue`
(verrouille l'invariant architectural).

## 12. Tests et résultats

Suite complète : **96 tests, 271 assertions, tous passants** (`php artisan test`).

Fichiers de tests créés pour cette fonctionnalité :

- `tests/Feature/MovementMemberTest.php` (23 tests) — page, soumission valide, validation des
  champs obligatoires, consentement obligatoire jamais pré-coché, case ATL facultative, non-
  validation automatique comme membre ATL, doublon par e-mail, doublon par WhatsApp,
  non-régression du statut ATL déjà tranché, normalisation WhatsApp, rejet WhatsApp invalide,
  honeypot, rate limiting, confirmation d'e-mail (token valide/invalide), désinscription, contenu de
  l'e-mail de bienvenue (section ATL, jamais « vous êtes membre »), accès admin, mise à jour de
  statut, acceptation ATL, soft delete/restauration, export CSV, masquage des listes.
- `tests/Feature/ContactMessageAdminTest.php` (10 tests) — sauvegarde avant envoi, panne SMTP sans
  perte de message ni 500, persistance réelle des jobs (non mockée), accès admin, marquage lu,
  changement de statut, renvoi manuel, suppression logique, destinataires corrects.
- `tests/Feature/AtlPageTest.php` (4 tests) — page ATL fonctionnelle sans album, liens vers le
  formulaire et Facebook, présence dans la navigation, restriction super-admin de `/admin/mail-test`.

La protection CSRF n'a pas de test dédié : Laravel désactive volontairement la vérification CSRF
pendant les tests unitaires (`PreventRequestForgery::runningUnitTests()`), un test qui prétendrait
la vérifier serait donc trompeur. Elle est garantie par construction : toutes les routes concernées
sont dans `routes/web.php`, dans le groupe de middleware `web` par défaut (qui inclut la vérification
CSRF), configuration confirmée dans `bootstrap/app.php`.

## 13. Validation réelle (smoke test) sur `http://tlt.test`

Effectuée avec de vraies requêtes HTTP (curl), pas seulement une lecture du code :

- Pages publiques `/`, `/mouvement`, `/atl`, `/rejoindre`, `/contact`, `/admin/login` : 200.
- Favicon, manifest, icônes Apple/Android : tous servis en 200.
- Assets compilés (`npm run build` relancé) : CSS/JS référencés dans le HTML et servis en 200.
- Inscription réelle au mouvement via HTTP (jeton CSRF récupéré puis soumis) : enregistrement
  correct en base, statuts `NEW`/`REQUESTED` (jamais `ACCEPTED` automatiquement), jeton de
  vérification généré.
- Traitement réel de la file d'attente (`php artisan queue:work`) : e-mail effectivement rendu et
  « envoyé » (mailer `log`), contenu vérifié dans `storage/logs/laravel.log`.
- Formulaire de contact réel : message enregistré, notification et accusé de réception réellement
  traités par les Jobs, horodatages persistés (après correction du bug §11.1).
- Connexion admin réelle (compte jetable), liste des membres, changement de statut ATL de
  `REQUESTED` à `ACCEPTED` via le formulaire admin réel, export CSV réel avec bon Content-Type et
  contenu correct, journal d'activité vérifié (`media_activity_logs`, `subject_type` =
  `MovementMember`, anciennes/nouvelles valeurs correctement enregistrées).
- Confirmation d'e-mail et désinscription réelles via les liens à jeton générés lors de
  l'inscription : passage correct à `CONFIRMED` puis `UNSUBSCRIBED`.

**Limite connue et assumée** : l'outil de navigateur intégré à cet environnement bloque les captures
d'écran/lecture de console sur le domaine personnalisé `tlt.test` derrière une carte d'approbation
manuelle que je ne peux pas m'accorder moi-même (déjà rencontré lors des phases précédentes). La
vérification visuelle "à l'œil" sur mobile n'a donc pas pu être réalisée directement ; elle a été
remplacée par une vérification au niveau du code (classes Tailwind responsives `sm:`/`lg:` déjà
utilisées de façon cohérente avec le reste du site, conteneur `.container-page` en largeur fluide
`max-w-7xl` sans largeur fixe qui provoquerait un débordement, balise viewport présente) et par
l'inspection HTTP directe du HTML produit. Ce point doit être considéré comme une vérification
partielle, pas une confirmation visuelle complète.

## 14. Procédure d'utilisation

**Traiter la file d'attente en local** : les jobs (e-mail de bienvenue, notifications de contact)
sont stockés dans la table `jobs` (`QUEUE_CONNECTION=database`) et ne sont **pas envoyés tant qu'un
worker n'a pas tourné** :

```
php artisan queue:work
```

En développement, il est normal qu'aucun worker ne tourne en permanence — les inscriptions restent
alors enregistrées en base (aucune perte), mais les e-mails de bienvenue/notification restent en
attente jusqu'au prochain passage du worker. Ne pas laisser cette dépendance implicite : en
production, prévoir un worker supervisé (systemd, Supervisor, ou équivalent hébergeur) qui relance
`queue:work` en continu.

**Activer l'envoi SMTP réel** : renseigner `MAIL_PASSWORD` dans `.env`, passer `MAIL_MAILER=smtp`,
puis tester via `/admin/mail-test` (super-admin) ou `php artisan mail:test votre@email.com` avant
toute mise en production.

**Gérer les inscriptions** : `/admin/movement-members` pour la liste et les statistiques,
changement de statut général et de statut ATL depuis la fiche détaillée. Une demande ATL doit être
explicitement passée à `ACCEPTED` par un administrateur — jamais automatique.

**Gérer les messages de contact** : `/admin/messages`, avec renvoi manuel en cas d'échec visible
(icône « échec » dans la liste).

## 15. Procédure de passage de SQLite à MySQL (documentée, non exécutée)

1. Créer la base MySQL/MariaDB cible et un utilisateur dédié.
2. Dans `.env` : `DB_CONNECTION=mysql`, `DB_HOST`, `DB_PORT=3306`, `DB_DATABASE`, `DB_USERNAME`,
   `DB_PASSWORD`.
3. Toutes les migrations existantes sont déjà compatibles MySQL/MariaDB (aucun SQL spécifique à
   SQLite, `enum()`/`softDeletes()`/index/unicité portables via le grammaire Laravel). Exécuter
   `php artisan migrate` sur la base cible.
4. Réimporter les données si nécessaire (`php artisan db:seed`, ou export/import applicatif) —
   aucune donnée n'est actuellement dépendante d'un chemin absolu vers le fichier SQLite.
5. Vérifier `config('media.archive_path')` (variable `MEDIA_ARCHIVE_PATH`) : ce chemin est déjà
   indépendant du moteur de base de données, aucune action requise.
6. Relancer la suite de tests contre la nouvelle configuration si souhaité (les tests utilisent une
   base SQLite en mémoire dédiée via `phpunit.xml`, indépendante de la base applicative).

## 16. Étapes manuelles restantes

- Renseigner `MAIL_PASSWORD` (mot de passe SMTP OVHcloud de `contact@telithotenge.com`) dans `.env`
  puis `MAIL_MAILER=smtp`, et tester réellement l'envoi via `/admin/mail-test`.
- Mettre en place un worker de file d'attente supervisé en production
  (`php artisan queue:work --daemon` sous Supervisor/systemd, ou équivalent hébergeur).
- Faire valider par Te Litho Tenge Didier ou son équipe les points listés dans
  [docs/recherches-atl.md](recherches-atl.md) §« Ce qui reste à faire valider » (structure juridique
  d'ATL, historique, activités propres, autorisation d'usage de photos Facebook).
- Vérification visuelle mobile/desktop complète via un vrai navigateur (limitation d'outil
  documentée en §13).
