Changement de modele:
- Le meme OrderNumber est partage entre Devis, Advert et Facture
(ex: 04/2026-00001 pour les 3)
- Les relations OrderNumber passent de OneToOne a ManyToOne pour
permettre le partage du meme numero
src/Entity/Order.php supprime, remplace par:
src/Entity/Facture.php (nouveau):
- orderNumber: ManyToOne vers OrderNumber (meme numero que l'Advert parent)
- advert: ManyToOne vers Advert (nullable)
- splitIndex: smallint, suffixe pour factures multiples sur un meme advert
(0 = pas de suffixe, 1 = -1, 2 = -2, etc.)
- getInvoiceNumber(): retourne le numero complet avec suffixe si splitIndex > 0
(ex: 04/2026-00001 ou 04/2026-00001-2)
src/Entity/Devis.php:
- orderNumber: OneToOne remplace par ManyToOne vers OrderNumber
- adverts: OneToMany vers Advert (inchange)
src/Entity/Advert.php:
- orderNumber: OneToOne remplace par ManyToOne vers OrderNumber
- orders: renomme en factures, OneToMany vers Facture
src/Repository/OrderRepository.php supprime, remplace par:
src/Repository/FactureRepository.php (nouveau)
migrations/Version20260402202809.php:
- Suppression table `order`, creation table facture
- Modification des contraintes unique sur devis et advert
(unique index supprime car ManyToOne)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Relations:
- Devis → OrderNumber (OneToOne): chaque devis a un numero unique
- Devis → Advert (OneToMany): un devis peut generer plusieurs factures pro forma
- Advert → OrderNumber (OneToOne): chaque facture pro forma a un numero unique
- Advert → Devis (ManyToOne, nullable): un advert peut exister sans devis
- Advert → Order (OneToMany): un advert peut generer plusieurs factures
- Order → OrderNumber (OneToOne): chaque facture a un numero unique
- Order → Advert (ManyToOne, nullable): une facture peut exister sans advert
Chaine: Devis → Advert → Order
Si un advert genere plusieurs factures, le champ splitIndex (smallint)
ajoute un suffixe -X au numero (ex: 04/2026-00001-1, 04/2026-00001-2).
La methode getInvoiceNumber() retourne le numero complet avec suffixe.
src/Entity/Devis.php: id, orderNumber (OneToOne), createdAt, adverts (OneToMany)
src/Entity/Advert.php: id, orderNumber (OneToOne), devis (ManyToOne nullable),
createdAt, orders (OneToMany)
src/Entity/Order.php: id, orderNumber (OneToOne), advert (ManyToOne nullable),
splitIndex (smallint default 0), createdAt, getInvoiceNumber()
Table nommee `order` (mot reserve SQL, echappee avec backticks)
src/Repository/DevisRepository.php, AdvertRepository.php, OrderRepository.php
migrations/Version20260402202554.php: tables devis, advert, `order` avec
foreign keys vers order_number et relations entre elles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/Admin/OrderNumberController.php (nouveau):
- Route /admin/numerotation, accessible ROLE_ROOT uniquement
- index(): affiche le prochain numero via OrderNumberService::preview()
et les 20 derniers numeros generes (ORDER BY id DESC)
- update(): modifie le prochain numero en creant une entree placeholder
avec le numero precedent (N-1) marque comme utilise, pour que le
prochain generate() retourne le numero souhaite
- Validation du format MM/YYYY-XXXXX via regex
- Verification que le numero n'existe pas deja
- Verification que le numero est au minimum 00001
templates/admin/order_number/index.html.twig (nouveau):
- Section "Prochain numero" : affiche le prochain numero en gros (gold)
avec formulaire pour le modifier (input avec pattern regex,
placeholder MM/YYYY-XXXXX, explication de l'utilite)
- Section "Derniers numeros generes" : tableau avec numero (font-mono),
date de creation, statut (Utilise vert / Reserve gris)
- Design glassmorphism (glass, input-glass, btn-gold, glass-dark header)
templates/admin/_layout.html.twig:
- Ajout du lien "Numerotation" dans la sidebar Super Admin avec
icone hash (#), route app_admin_order_number, style active-danger
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/OrderNumberService.php (nouveau):
- Format: MM/YYYY-XXXXX (ex: 04/2026-00001, 04/2026-00002, etc.)
- Le compteur XXXXX est par mois: reset a 00001 a chaque nouveau mois
- generate(): cherche le dernier numOrder du mois courant via LIKE
'MM/YYYY-%' ORDER BY DESC, extrait le compteur, incremente de 1,
pad sur 5 chiffres, persiste et flush le nouvel OrderNumber
- generateAndUse(): genere + markAsUsed() en une operation
- preview(): retourne le prochain numero sans le creer en BDD
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Entity/OrderNumber.php (nouveau):
- id: int auto-increment
- numOrder: string(50) unique, le numero de commande
- createdAt: DateTimeImmutable, date de creation (auto dans le constructeur)
- isUsed: bool, false par defaut, marque via markAsUsed()
src/Repository/OrderNumberRepository.php (nouveau):
- Repository Doctrine standard pour OrderNumber
migrations/Version20260402201935.php:
- Table order_number avec index unique sur num_order
- created_at TIMESTAMP, is_used BOOLEAN
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
config/packages/cache.yaml:
- Ajout du pool dns_infra_cache avec adapter cache.app et TTL 3600s
- Le pool etait dans config/packages/packages/cache.yaml qui est
surcharge par config/packages/cache.yaml, donc le service
dns_infra_cache n'existait pas
config/packages/packages/cache.yaml:
- Suppression du pool dns_infra_cache (doublon)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/DnsReportController.php:
- Injection du pool cache dns_infra_cache via #[Autowire]
- Les resultats des checks sont caches avec la cle dns_infra_check_{token}
pendant 1 heure (3600s) pour eviter de rappeler toutes les APIs
(Cloudflare, AWS SES, Mailcow, RDAP, dig) a chaque rechargement
- La date du rapport est stockee dans le cache au format ISO 8601
config/packages/packages/cache.yaml:
- Nouveau pool dns_infra_cache sur Redis avec default_lifetime 3600s
src/Command/PurgeEmailTrackingCommand.php (nouveau):
- Commande app:email-tracking:purge qui supprime les EmailTracking
dont sentAt est anterieur au seuil (90 jours par defaut)
- Option --days pour changer la retention (ex: --days=30)
- Utilise une requete DQL DELETE pour performance
ansible/deploy.yml.disabled:
- Nouveau cron "crm-siteconseil email-tracking purge": tous les jours
a 5h du matin, supprime les EmailTracking de plus de 90 jours
docker/cron/entrypoint.sh:
- Liste complete des taches cron mise a jour avec:
- */5 min: expire-pending, infra:snapshot
- */15 min: services:check
- toutes les heures: monitor:messenger
- toutes les 2h: dns:check
- toutes les 6h: stripe:sync
- 3h: meilisearch consistency
- 4h: attestations clean
- 5h: email-tracking purge
- 6h: cloudflare clean
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/DnsCheckService.php:
- RDAP_SERVERS['dev']: URL corrigee de rdap.nic.google (n'existe pas)
vers pubapi.registry.google/rdap/domain/ (endpoint officiel Google
Registry trouve via le bootstrap IANA data.iana.org/rdap/dns.json)
- esy-web.dev retourne maintenant: expiration 2028-01-08,
NS candy.ns.cloudflare.com + nero.ns.cloudflare.com
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/DnsCheckService.php:
- Nouvelle methode checkWhois() qui verifie pour chaque domaine:
- Nameservers via dig @1.1.1.1 NS: verifie que les NS contiennent
*.ns.cloudflare.com, erreur si non Cloudflare
- Expiration via RDAP (protocole standard ICANN remplacant whois):
erreur si expire ou moins de 30 jours restants,
warning si moins de 90 jours, OK sinon
- Constante RDAP_SERVERS avec les serveurs RDAP par TLD
(.fr → rdap.nic.fr, .com → rdap.verisign.com, .dev → rdap.nic.google,
.org/.eu → rdap.org, fallback rdap.org)
- Methode queryRdap() qui interroge le serveur RDAP du TLD avec
Accept: application/rdap+json et timeout 10s
- Methode getNsRecords() qui parse la sortie dig NS
- Ajout du type NS dans le fallback dns_get_record
src/Command/CheckDnsCommand.php:
- Appel de checkWhois() apres checkBounce() pour chaque domaine
src/Controller/DnsReportController.php:
- Meme appel checkWhois() dans le rapport web
Seuils d'alerte:
- < 0 jours : erreur "EXPIRE"
- < 30 jours : erreur "X jours restants"
- < 90 jours : warning "X jours restants"
- > 90 jours : OK "X jours restants"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Command/CheckDnsCommand.php:
- Suppression du check 'Mailcow DKIM: Ignore (DKIM via AWS SES)'
et du succes associe (le DKIM est deja verifie par les 3 checks
AWS SES DKIM CNAME, cette ligne etait redondante)
src/Controller/DnsReportController.php:
- Meme suppression dans le controller du rapport web
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Command/CheckDnsCommand.php:
- Nouvelle methode getActualDnsValue() qui retourne la vraie valeur DNS
selon le type (MX: liste des targets, CNAME: target, TXT: contenu,
SRV: target:port) au lieu de "Trouve"/"Non trouve"
- Methodes getFirstTxtValue() et getSrvValue() pour recuperer les
valeurs reelles TXT et SRV via dig @1.1.1.1
- Les checks Mailcow DNS utilisent maintenant getActualDnsValue()
dans la colonne dig: mail.esy-web.dev, v=STSv1;id=..., etc.
src/Controller/DnsReportController.php (reecrit completement):
- Memes methodes enrichWithCloudflare() et enrichLastCheck() que la commande
- checkAwsSes(): enrichissement CF sur chaque DKIM CNAME
({token}._domainkey.{domain}), MAIL FROM MX et MAIL FROM TXT
- checkMailcow(): enrichissement CF sur chaque record attendu
(autodiscover, autoconfig, SRV, _mta-sts, mta-sts)
- getActualDnsValue(), getMxValues(), getFirstTxtValue(), getSrvValue()
pour afficher les vraies valeurs dig dans la colonne Dig
- Bounce: enrichissement CF sur bounce.{domain} MX
- La page web /email/configuration/{token} affiche maintenant les
3 colonnes correctement remplies pour tous les enregistrements
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Command/CheckDnsCommand.php:
- Nouvelle methode enrichLastCheck() qui cherche un record dans les
records Cloudflare par nom+type et remplit cloudflare/cf_status
sur le dernier check ajoute au tableau
- Bounce: enrichissement CF sur bounce.{domain} type MX
- AWS SES DKIM CNAME: enrichissement CF sur {token}._domainkey.{domain}
type CNAME pour chacun des 3 tokens DKIM
- AWS SES MAIL FROM MX: enrichissement CF sur bounce.{domain} type MX
- AWS SES MAIL FROM TXT: enrichissement CF sur bounce.{domain} type TXT
- Mailcow DNS: enrichissement CF pour chaque record attendu
(autodiscover CNAME, autoconfig CNAME, _autodiscover._tcp SRV,
_mta-sts TXT, mta-sts CNAME) avec le nom et type exacts
- checkAwsSes() et checkMailcow() recoivent maintenant $cfRecords
en parametre pour effectuer les enrichissements
La colonne Cloudflare du rapport web et email affiche maintenant
la valeur presente dans la zone CF pour bounce, DKIM CNAME,
MAIL FROM, autodiscover, autoconfig, mta-sts au lieu de "Non trouve"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ansible/deploy.yml.disabled:
- Nouveau cron "crm-siteconseil dns check": execute app:dns:check
toutes les 2 heures (minute: 0, hour: */2), envoie le rapport
DNS par email a monitor@siteconseil.fr et notification Discord
si erreurs detectees, log dans /var/log/crm-siteconseil-dns-check.log
- Nouveau cron "crm-siteconseil cloudflare clean": execute
app:cloudflare:clean tous les jours a 6h du matin (minute: 0,
hour: 6), supprime les enregistrements TXT _acme-challenge
obsoletes dans toutes les zones Cloudflare, log dans
/var/log/crm-siteconseil-cloudflare-clean.log
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Command/CloudflareDnsCleanCommand.php (nouveau):
- Commande app:cloudflare:clean avec 3 options:
--list-only: affiche uniquement le tableau des zones (nom, ID, statut, plan)
--zone=domain.fr: filtre sur une seule zone
--dry-run: affiche les records a supprimer sans les supprimer
- Sans option: parcourt toutes les zones, trouve les TXT _acme-challenge
et les supprime via l'API Cloudflare
- Affiche le nombre de records trouves et supprimes par zone
- Affiche le total global a la fin
src/Service/CloudflareService.php:
- listZones(): liste toutes les zones du compte avec pagination
(retourne nom, id, status, plan pour chaque zone)
- deleteDnsRecord(): supprime un enregistrement DNS par zoneId + recordId
via DELETE /zones/{zoneId}/dns_records/{recordId}
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/CloudflareService.php - getDnsRecords():
- La zone esy-web.dev contient plus de 100 records DNS (26 TXT +
MX + CNAME + A + AAAA etc.), la requete per_page=100 sans
pagination ne retournait pas les TXT (SPF, DMARC, _mta-sts)
- Ajout d'une boucle de pagination: recupere page par page
jusqu'a total_pages via result_info.total_pages
- Les colonnes Cloudflare du rapport affichaient "Non trouve"
alors que les records existaient bien dans la zone
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/DnsReportController.php:
- Remplacement de \$this->container->get('doctrine')->getManager()
par injection de EmailTrackingRepository dans les parametres de __invoke()
- Le service locator du controller ne contient pas 'doctrine',
l'injection de dependance est la bonne pratique Symfony
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Controller/DnsReportController.php (nouveau):
- Route /email/configuration/{token} accessible via le lien dans le mail
- Utilise le messageId de l'EmailTracking comme token d'acces
(seuls les destinataires du mail ont le lien)
- Execute tous les checks en temps reel: DnsCheckService (SPF, DMARC, MX,
Bounce via dig @1.1.1.1), AwsSesService (domaine, 3 DKIM CNAME,
MAIL FROM MX/TXT, bounce notif), CloudflareService (zone, records),
MailcowService (domaine, DKIM, MX, autodiscover, autoconfig, SRV, MTA-STS)
- Enrichit chaque check avec la colonne Cloudflare
- Passe les resultats au template Twig pour affichage complet
templates/dns_report/index.html.twig (nouveau):
- Page glassmorphism avec header glass
- Resume en haut: 3 cards (verifications OK, erreurs, avertissements)
avec bordures laterales colorees vert/rouge/jaune
- Tableau par domaine avec 6 colonnes: Source (badge colore par type:
orange AWS, violet Mailcow, bleu Cloudflare, gris DNS), Verification,
Attendu, Dig (actuel), Cloudflare, Statut (rond colore)
- Section erreurs detaillees avec liste
- Section avertissements avec liste
- Footer "Esy-Infra - Service de monitoring d'infra"
templates/emails/dns_report.html.twig (simplifie):
- Mail ne contient plus les details: seulement un tableau avec
chaque domaine et son statut (OK vert / WARN jaune / KO rouge)
- Bouton "Voir le rapport complet" avec lien vers la page web
(VML fallback pour Outlook)
- Le lien utilise le placeholder __DNS_REPORT_URL__ remplace par
le MailerService avec le messageId du mail
src/Service/MailerService.php:
- Ajout du remplacement de __DNS_REPORT_URL__ par l'URL absolue
/email/configuration/{messageId} dans sendEmail(), au meme
endroit que __VIEW_URL__
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/DnsCheckService.php:
- Constante RESOLVER = '1.1.1.1' (Cloudflare DNS)
- Methode dig() utilise la commande dig @1.1.1.1 pour toutes les
requetes DNS afin d'avoir des resultats coherents quel que soit
le resolver local du serveur
- isDigAvailable(): detecte si dig est installe (cache static)
- fallbackDnsGetRecord(): quand dig n'est pas installe, utilise
dns_get_record() PHP natif et formate la sortie au format dig
+noall +answer pour que le parsing reste identique
- getTxtRecords(), getCnameRecord(), getMxRecords(), getSrvRecords()
utilisent tous dig() en interne
- getCnameRecord() et getSrvRecords() rendues publiques pour utilisation
par la commande
src/Command/CheckDnsCommand.php:
- Suppression du check DKIM generique (DKIM verifie uniquement via
AWS SES avec les 3 CNAME individuels par domaine)
- checkDnsRecordExists(), checkMxExists(), checkTxtContains() utilisent
maintenant $this->dnsCheck au lieu de dns_get_record() direct
- getCnameRecord() supprimee de la commande (delegue au service)
- getMxValues() et getTxtSpfValue() utilisent le service
docker/php/dev/Dockerfile:
- Ajout du paquet dnsutils (fournit la commande dig)
docker/php/prod/Dockerfile:
- Ajout du paquet dnsutils (fournit la commande dig)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/MailcowService.php - getExpectedDnsRecords() reecrit:
- Suppression des checks SPF et DMARC (deja verifies par DnsCheckService)
- Suppression du check DKIM TXT (le DKIM est gere par AWS SES, pas Mailcow)
- Ajout du champ 'optional' (bool) dans chaque enregistrement attendu
au lieu de deviner l'optionnalite par le nom
- Enregistrements verifies:
- MX {domain} → mail.esy-web.dev (obligatoire)
- CNAME autodiscover.{domain} → mail.esy-web.dev (obligatoire)
- CNAME autoconfig.{domain} → mail.esy-web.dev (obligatoire)
- SRV _autodiscover._tcp.{domain} (optionnel)
- TXT _mta-sts.{domain} v=STSv1 (optionnel)
- CNAME mta-sts.{domain} → mail.esy-web.dev (optionnel)
src/Command/CheckDnsCommand.php - checkMailcow():
- DKIM Mailcow marque comme OK avec detail "Ignore (DKIM via AWS SES)"
car l'envoi des mails utilise le DKIM d'AWS SES, pas celui de Mailcow
- Suppression de la methode getDkimFromDns() devenue inutile
- Utilisation du champ 'optional' de getExpectedDnsRecords() au lieu
de deviner par le nom de l'enregistrement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/MailcowService.php:
- getDomain(): l'API Mailcow retourne un objet JSON {} pour un seul
domaine (pas un tableau [{...}]). Ajout de la detection: si $data[0]
existe c'est un tableau, sinon c'est directement l'objet domaine
- getDomainStatus(): la cle du nombre de boites mail est
'mboxes_in_domain' (pas 'mbox_count' qui n'existe pas dans la reponse)
Resultat: Mailcow affiche maintenant correctement:
- siteconseil.fr: actif, 28 boites mail
- esy-web.dev: actif, 20 boites mail
- Autodiscover, autoconfig, SRV, MTA-STS: tous OK pour les 2 domaines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/AwsSesService.php:
- Ajout methode getMailFromStatus() qui recupere via l'API SES:
- mail_from_domain: le sous-domaine MAIL FROM (ex: bounce.siteconseil.fr)
- mail_from_status: statut de verification (Success/Pending/Failed)
- mx_expected: le MX attendu (feedback-smtp.{region}.amazonses.com)
- txt_expected: le SPF attendu (v=spf1 include:amazonses.com ~all)
Les valeurs sont specifiques a chaque domaine et region AWS
src/Command/CheckDnsCommand.php - methode checkAwsSes() reecrite:
- Verification domaine: attendu="Success", dig=statut reel
- DKIM statut global: attendu="Enabled=oui, Verified=oui", dig=statut reel
- 3 DKIM CNAME individuels: pour chaque token retourne par SES,
verifie que {token}._domainkey.{domain} CNAME {token}.dkim.amazonses.com
existe dans le DNS. Attendu=CNAME cible, Dig=valeur trouvee ou "Non trouve"
- MAIL FROM: attendu=sous-domaine configure dans SES, dig=statut
- MAIL FROM MX: attendu="{bounce.domain} MX feedback-smtp.{region}.amazonses.com",
dig=MX reel trouve
- MAIL FROM TXT: attendu="{bounce.domain} TXT v=spf1 include:amazonses.com ~all",
dig=enregistrement SPF reel trouve
- Bounce notifications: attendu="Forwarding ou SNS topic", dig=config reelle
- Ajout methodes getMxValues() et getTxtSpfValue() pour recuperer les
valeurs reelles du DNS a afficher dans la colonne Dig
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/CloudflareService.php:
- isAvailable(): remplace l'appel /user/tokens/verify par /zones?per_page=1
car certains tokens API Cloudflare n'ont pas le scope User:Read
necessaire pour /user/tokens/verify mais ont bien le scope Zone:DNS:Read
- Verifie maintenant que le champ 'success' est true dans la reponse
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Command/CheckDnsCommand.php:
- Injection de HttpClientInterface et kernel.environment dans le constructeur
- Constante DISCORD_WEBHOOK avec l'URL du webhook Discord
- sendReport(): appel sendEmail avec priority=1 (HIGH) pour que le mail
soit marque comme urgent dans tous les clients mail
- sendDiscordNotification(): envoie un embed Discord avec:
- Titre "Esy-Infra : ALERTE DNS" (rouge), "DNS avertissements" (jaune)
ou "DNS Configuration OK" (vert)
- Description avec le nombre de succes/erreurs/warnings
- Les 5 premieres erreurs en citation markdown
- Footer "Esy-Infra - Monitoring DNS" avec timestamp
- La notification Discord est envoyee UNIQUEMENT en environnement prod
(condition sur kernel.environment == 'prod')
src/Service/MailerService.php:
- Ajout du parametre $priority (int, defaut 3 = normal) a sendEmail()
- Appel de ->priority($priority) sur l'objet Email Symfony
- Priority 1 = highest, 2 = high, 3 = normal, 4 = low, 5 = lowest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/DnsCheckService.php:
- Methode check() enrichie avec 4 nouveaux champs: expected (valeur attendue),
dig (valeur actuelle trouvee par dig), cloudflare (valeur dans la zone CF),
cf_status (statut de la colonne CF: ok/error/vide)
- checkSpf(): expected = "include:X dans le SPF", dig = contenu SPF complet
- checkDmarc(): expected = "p=reject ou p=quarantine", dig = contenu DMARC
- checkDkim(): expected = "FQDN CNAME/TXT", dig = cible CNAME ou debut TXT
- checkMx(): expected = MX attendu, dig = liste des MX trouves avec priorite
- checkBounce(): expected = "feedback-smtp.*.amazonses.com", dig = valeur trouvee
src/Command/CheckDnsCommand.php:
- Nouveau champ MONITOR_EMAIL = 'monitor@siteconseil.fr' pour l'envoi du rapport
- loadCloudflareRecords(): charge les records CF une seule fois par domaine
au debut de l'execution, retourne un array indexe par domaine
- enrichWithCloudflare(): apres chaque check DNS, parcourt les records CF
pour trouver l'enregistrement correspondant et remplir les colonnes
cloudflare et cf_status dans chaque check
- checkAwsSes(): utilise DnsCheckService::check() avec expected/dig
(ex: expected="Success", dig="Absent" pour la verification domaine)
- checkMailcow(): utilise DnsCheckService::check() avec expected/dig
(ex: expected="Cle Mailcow: abc...", dig="Cle DNS: xyz..." pour DKIM)
- sendReport(): envoie a MONITOR_EMAIL au lieu de l'admin email
templates/emails/dns_report.html.twig:
- Tableau par domaine avec 6 colonnes: Type, Check, Attendu, Dig (actuel),
Cloudflare, Statut (OK/erreur/warning)
- Colonne Dig coloree en vert/rouge/jaune selon le statut du check
- Colonne Cloudflare coloree selon cf_status
- Colonnes avec word-break: break-all pour les longues valeurs DNS
- Bandeau resume en haut avec compteurs succes/erreurs/warnings
avec bordures laterales colorees
- Pied de mail: "Rapport par Esy-Infra - Service de monitoring d'infra"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Service/MailcowService.php (nouveau):
- Connexion a l'API Mailcow via X-API-Key header
- getDomains(): liste tous les domaines configures
- getDomain(): informations d'un domaine specifique
- getDomainStatus(): statut actif, nombre de boites, quota, quota utilise
- getDkimKey(): recupere la cle DKIM TXT configuree dans Mailcow
- getExpectedDnsRecords(): retourne la liste des enregistrements DNS
attendus par Mailcow pour un domaine (MX, SPF, DMARC, DKIM,
autodiscover CNAME, autoconfig CNAME, SRV _autodiscover, _mta-sts TXT)
- getMailboxes(): liste les boites mail d'un domaine
- isAvailable(): test de connectivite API via /api/v1/get/status/containers
src/Command/CheckDnsCommand.php:
- Ajout de MailcowService dans le constructeur
- Nouvelle methode checkMailcow() qui:
- Verifie si le domaine existe et est actif dans Mailcow
- Recupere la cle DKIM Mailcow et la compare avec celle du DNS
(comparaison partielle des 40 premiers caracteres)
- Verifie chaque enregistrement DNS attendu par Mailcow:
- MX, SPF, DMARC, DKIM : marques comme erreur si absents
- autodiscover, autoconfig, SRV, _mta-sts : marques comme
warning (optionnels)
- Methodes utilitaires: getDkimFromDns(), checkDnsRecordExists(),
checkMxExists(), checkTxtContains(), getCnameRecord()
Variables d'environnement:
- .env: MAILCOW_URL=https://mail.esy-web.dev, MAILCOW_API_KEY (vide)
- .env.local: MAILCOW_API_KEY=DF0E7E-0FD059-16226F-8ECFF1-E558B3
- ansible/vault.yml: mailcow_api_key ajoutee
- ansible/env.local.j2: MAILCOW_URL et MAILCOW_API_KEY ajoutees
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Architecture:
- Les domaines (siteconseil.fr, esy-web.dev) sont definis en constante
dans la commande uniquement, pas dans les services
- 3 services independants reutilisables:
src/Service/DnsCheckService.php (nouveau):
- Methodes publiques checkSpf(), checkDmarc(), checkDkim(), checkMx(),
checkBounce() qui prennent le domaine en parametre
- Verification SPF: presence des includes amazonses.com et mail.esy-web.dev
- Verification DMARC: politique, presence de rua
- Verification DKIM: test de 10 selecteurs en CNAME et TXT
- Verification MX: le MX attendu est passe en parametre par la commande
- Verification Bounce: MX/CNAME/TXT sur bounce.*
src/Service/AwsSesService.php (nouveau):
- Authentification AWS Signature V4 via HTTP direct (pas de SDK)
- isDomainVerified(): verification du statut du domaine dans SES
- getDkimStatus(): statut DKIM (enabled, verified, tokens)
- getNotificationStatus(): bounce_topic, complaint_topic, forwarding
- listVerifiedIdentities(): liste des domaines verifies
- isAvailable(): test de connectivite API
src/Service/CloudflareService.php (nouveau):
- Authentification Bearer token via HTTP direct (pas de SDK)
- getZoneId(): recupere le zone ID dynamiquement par nom de domaine
(plus besoin de CLOUDFLARE_ZONE_ID en dur)
- getDnsRecords(): tous les enregistrements d'une zone
- getDnsRecordsByType(): filtrage par type (TXT, MX, CNAME...)
- getZone(): informations d'une zone
- isAvailable(): verification du token API
src/Command/CheckDnsCommand.php (reecrit):
- Utilise les 3 services pour orchestrer les verifications
- Affichage console colore avec icones OK/ERREUR/ATTENTION
- Envoie un rapport email via le template Twig dns_report.html.twig
templates/emails/dns_report.html.twig (nouveau):
- Template email compatible tous clients (table-based, CSS inline,
margin/padding longhand, mso-line-height-rule, pas de rgba/border-radius)
- Bandeau colore vert/jaune/rouge selon le statut global
- Section succes avec checkmarks verts dans un tableau alterne
- Section erreurs en rouge avec croix dans un tableau fond #fef2f2
- Section avertissements en jaune avec triangles fond #fffbeb
- Detail par domaine avec tableau type/verification/statut
- Utilise le template email/base.html.twig (header gold, footer dark)
Variables d'environnement ajoutees:
- .env: AWS_PK, AWS_SECRET, AWS_REGION (eu-west-3), CLOUDFLARE_KEY (vides)
- .env.local: valeurs reelles des cles AWS et Cloudflare
- ansible/vault.yml: aws_pk, aws_secret, cloudflare_key
- ansible/env.local.j2: AWS_PK, AWS_SECRET, AWS_REGION, CLOUDFLARE_KEY
avec references au vault
- CLOUDFLARE_ZONE_ID supprime (recupere dynamiquement via l'API)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Command/CheckDnsCommand.php (nouveau fichier):
- Commande Symfony app:dns:check qui verifie les DNS de siteconseil.fr
et esy-web.dev
- Verification SPF: presence des includes amazonses.com et mail.esy-web.dev,
terminaison -all ou ~all
- Verification DMARC: presence sur _dmarc.*, politique p=reject/quarantine/none,
presence de rua (adresse de rapport)
- Verification DKIM: test de 10 selecteurs (ses1/ses2/ses3, default, mail, k1,
google, selector1/selector2, dkim) en CNAME et TXT
- Verification MX: presence de mail.esy-web.dev comme serveur de reception
- Verification Bounce: enregistrements MX/CNAME/TXT sur bounce.*,
verification du SPF bounce avec include:amazonses.com
- Envoi d'un email de rapport complet a l'admin avec:
- Bandeau colore vert (OK) / jaune (warnings) / rouge (erreurs)
- Tableau des verifications reussies avec checkmarks verts
- Tableau des erreurs en rouge avec croix
- Tableau des avertissements en jaune avec triangles
- Date du rapport et liste des domaines verifies
- Le rapport est envoye dans tous les cas (succes ou echec)
- Retourne FAILURE si au moins une erreur est detectee
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gmail, ProtonMail, SFR, Yahoo, Outlook.com, Orange et d'autres clients
email suppriment ou remplacent la balise <body> par un <div>, ce qui
fait perdre tous les styles inline definis sur le body.
templates/email/base.html.twig:
- Table wrapper principale: ajout de margin-top/right/bottom/left: 0,
padding-top/right/bottom/left: 0, font-family: Arial, color: #111827,
-webkit-text-size-adjust: 100%, -ms-text-size-adjust: 100%
en plus du background-color: #eeeef3 deja present
- Les styles restent aussi sur le body pour les clients qui le supportent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Outlook Windows (2007-2019) ne supporte pas la propriete height sur
les elements div, span et p. AOL et Yahoo remplacent height par
min-height ce qui casse le rendu.
templates/emails/revendeur_created.html.twig:
- 2 div spacers <div style="height: 8px;"> remplaces par des table
spacers avec td font-size: 1px et mso-line-height-rule: exactly
line-height: 8px + pour forcer la hauteur sur tous les clients
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/Command/TestMailCommand.php (nouveau fichier):
- Commande Symfony app:mail:test <email> --mode=dev|prod
- Envoie un email de test complet pour verifier le rendu sur tous les clients
- Option --mode pour afficher un bandeau DEV (jaune) ou PROD (rouge)
templates/emails/test_mail.html.twig (nouveau fichier):
- Bandeau environnement colore (dev: #f59e0b jaune, prod: #dc2626 rouge)
- Bloc info sombre avec destinataire, environnement et date/heure
- Tableau de donnees avec 3 services (Esy-Web, Esy-Mail, Esy-Defender Pro),
tarifs, statuts (ACTIF vert, IMPAYE rouge) et ligne TOTAL
- Liste de verification des styles: gras, italique, souligne, lien, couleurs
- Bloc alerte avec bordure laterale gold et fond gris
- 2 boutons centres (Gold #fabf04 et Dark #111827) avec fallback VML
pour Outlook via v:roundrect
- Tableau des 5 applications SITECONSEIL avec liens et descriptions
- Simulation de fichier joint avec icone trombone
- Pied de mail avec mention de la commande utilisee
- Tout en CSS inline compatible: padding longhand, margin longhand,
mso-line-height-rule:exactly, line-height en px, background-color,
pas de rgba/border-radius/box-shadow
templates/emails/test_mail.html.twig:
- Balise <u> remplacee par span avec text-decoration: underline en inline
(la balise <u> n'est pas supportee par ProtonMail, Orange iOS, SFR iOS)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
.gitea/workflows/discord-notify.yml:
- Ajout du champ "Applications SITECONSEIL" dans l'embed Discord
avec 5 liens cliquables et leur description:
- crm.siteconseil.fr: plateforme de gestion clients et revendeurs
- sign.siteconseil.fr: signature electronique des documents (DocuSeal)
- payment.siteconseil.fr: portail de paiement securise (Stripe)
- status.siteconseil.fr: page de status et disponibilite des services
- stripe.siteconseil.fr: reception des webhooks Stripe et redirection
vers le dashboard Stripe Connect des revendeurs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Outlook Windows (2007-2019) a un bug avec line-height en em et sans unite:
le rendu est imprevisible. La solution est d'utiliser des valeurs en px
avec mso-line-height-rule:exactly.
templates/email/base.html.twig:
- cellule contenu: line-height: 1.6 remplace par line-height: 22px
avec ajout de mso-line-height-rule: exactly
templates/emails/2fa_code.html.twig:
- 3 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
templates/emails/forgot_password_code.html.twig:
- 3 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
templates/emails/membre_created.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
- 2 listes ul: line-height: 1.8 remplace par 25px (font 14px)
et 23px (font 13px) + mso-line-height-rule
templates/emails/password_changed.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
templates/emails/revendeur_created.html.twig:
- 5 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
templates/emails/rgpd_access.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
templates/emails/rgpd_attestation_signed.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
templates/emails/rgpd_deletion.html.twig:
- 4 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
templates/emails/rgpd_no_data.html.twig:
- 5 paragraphes: line-height: 1.6 remplace par 22px + mso-line-height-rule
Suppression de box-shadow dans tous les templates email (non supporte
par Outlook Windows, Orange, GMX, WEB.DE, Samsung Email).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Outlook Windows (2007-2019) a un bug ou le padding vertical d'une cellule
est applique a toutes les cellules de la meme ligne avec la plus grande
valeur. SFR, Samsung Email, GMX, WEB.DE, HEY ne supportent que
partiellement le shorthand.
templates/emails/2fa_code.html.twig:
- span code: padding: 16px 32px remplace par padding-top/right/bottom/left
templates/emails/forgot_password_code.html.twig:
- span code: padding: 16px 32px remplace par padding-top/right/bottom/left
templates/emails/membre_created.html.twig:
- bloc identifiants: padding: 20px remplace par les 4 proprietes longhand
- 2 divs internes: padding: 4px 0 remplace par longhand
- bouton connexion: padding: 12px 24px remplace par longhand
templates/emails/revendeur_created.html.twig:
- bloc identifiants: padding: 20px remplace par longhand
- 3 blocs etapes: padding: 12px 16px remplace par longhand
- bouton mot de passe: padding: 14px 24px remplace par longhand
- bouton connexion: padding: 12px 24px remplace par longhand
- bloc info: padding: 16px remplace par longhand
- 3 divs internes: padding: 2px 0 remplace par longhand
templates/emails/rgpd_attestation_signed.html.twig:
- 3 blocs info: padding: 8px 12px remplace par longhand
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Outlook Windows (2007-2019) ne supporte que background-color, pas
le shorthand background. rgba() et border-radius ne sont pas non plus
supportes par Outlook Windows Mail, GMX, Samsung Email, Orange.
templates/emails/2fa_code.html.twig:
- code de verification: background: #111827 remplace par background-color: #111827
templates/emails/forgot_password_code.html.twig:
- code de verification: background: #111827 remplace par background-color: #111827
templates/emails/membre_created.html.twig:
- bloc identifiants: background: #111827 remplace par background-color: #111827
- bouton connexion: background: #fabf04 remplace par background-color: #fabf04,
border rgba(0,0,0,0.1) remplace par #e5e5e5, border-radius: 8px supprime
templates/emails/revendeur_created.html.twig:
- bloc identifiants: background: #111827 remplace par background-color: #111827
- 3 blocs etapes: background: #f9fafb remplace par background-color: #f9fafb
- bouton mot de passe: background: #fabf04 remplace par background-color: #fabf04,
border rgba supprime, border-radius supprime
- bouton connexion: background: #fff remplace par background-color: #ffffff,
border rgba supprime, border-radius supprime
- bloc info: background: #f9fafb remplace par background-color: #f9fafb
templates/emails/rgpd_attestation_signed.html.twig:
- 2 blocs info: background: #f9fafb remplace par background-color: #f9fafb
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
templates/email/base.html.twig:
- Remplacement de la structure div par une structure table-based (role=presentation)
pour compatibilite Outlook/Orange/SFR/Yahoo
- Suppression de rgba(), linear-gradient(), border-radius, box-shadow
(non supportes par Outlook Windows 2007-2019)
- Ajout du doctype XHTML 1.0 Transitional pour Outlook
- Ajout du bloc conditionnel <!--[if mso]> pour forcer Arial sur Outlook
- Remplacement de margin: 0 par margin-top/right/bottom/left: 0
(margin shorthand non supporte par Orange, SFR, certains Outlook)
- Remplacement de padding shorthand par padding-top/right/bottom/left
dans le body et les cellules principales
- Ajout de -webkit-text-size-adjust et -ms-text-size-adjust sur body
- Logo: ajout de width et height auto explicites + border: 0
- Fond semi-transparent rgba(255,255,255,0.92) remplace par #ffffff opaque
- Gradient gold remplace par background-color: #fabf04 fixe
- Couleur footer rgba(255,255,255,0.7) remplacee par #b0b0b0 fixe
- Entite • remplacee par • pour compatibilite XHTML
templates/emails/*.html.twig (9 fichiers):
- Remplacement de toutes les proprietes margin shorthand par les
proprietes longhand margin-top/margin-right/margin-bottom/margin-left
dans 2fa_code, forgot_password_code, membre_created, password_changed,
revendeur_created, rgpd_access, rgpd_attestation_signed, rgpd_deletion,
rgpd_no_data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Problem: OAuth login failed because the authenticator was checking for
old Keycloak group names (super_admin_asso, gp_member) that no longer
exist in the master realm.
Changes:
- src/Security/KeycloakAuthenticator.php:106: resolveRoles() now checks
for 'siteconseil_admin' instead of 'super_admin_asso' to grant ROLE_ROOT
- src/Controller/Admin/MembresController.php:140: member creation role
resolution updated from 'super_admin_asso' to 'siteconseil_admin'
- templates/admin/membres.html.twig: checkbox values updated from
'gp_member' to 'siteconseil_member' and 'super_admin_asso' to
'siteconseil_admin' in the member management form
- assets/app.js:5-6: JS mutual exclusion logic updated to use new
group values 'siteconseil_member' and 'siteconseil_admin'
- tests/Security/KeycloakAuthenticatorTest.php:79: test data updated
from 'super_admin_asso' to 'siteconseil_admin'
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove jq dependency
- Use file-based payload with cat > /tmp/discord.json instead of inline HEREDOC
- Fix HEREDOC indentation issue
- Escape JSON properly with sed
- Use curl -d @file for reliable payload delivery
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Email templates: replace table/tr/td with div-based CSS layout
- PDF templates: replace table with display:table/table-row/table-cell CSS (Dompdf compatible)
- Only table.data (RGPD access session data) remains as actual HTML table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add thead/th to commissions table in tarif.html.twig
- Add th headers to contrat_revendeur parties table
- Add role=presentation to PDF layout tables (info-grid, verify-box, signatures)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace email layout tables with CSS divs in base.html.twig
- Replace deprecated width/align attributes with CSS styles in PDF templates
- Add role="presentation" to email layout tables
- Convert td to th[scope=row] in profil and attestation verify tables
- Reduce function nesting in app.js by extracting renderHit/performSearch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add for/id attributes to all form labels for accessibility compliance
- Add <title> tags to PDF templates (rgpd_access, rgpd_no_data, rgpd_deletion, contrat_revendeur)
- Add role="presentation" to email layout tables
- Remove deprecated cellpadding/cellspacing attributes from all templates
- Fix PHPUnit notices by replacing createMock with createStub where no expectations are set
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ForgotPasswordController::validateResetInput: reduce from 4 to 3 returns using match expression
- DocuSealService::downloadSignedDocuments: reduce from 4 to 3 returns by extracting fetchAndStoreDocuments
- User: add @SuppressWarnings for getEmailAuthRecipient/getGoogleAuthenticatorUsername (required by 2FA interfaces)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reduce cognitive complexity and fix code smells across multiple files:
- Extract helper methods in DocuSealService, ForgotPasswordController, WebhookDocuSealController
- Reduce MembresController.persistLocalUser from 8 to 3 parameters using typed array
- Replace chained if/returns with ROLE_ROUTES map in LoginSuccessHandler
- Add 100% test coverage for AnalyticsCryptoService (15 tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>