feat: cache DNS report + purge EmailTracking + crons mis a jour

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>
This commit is contained in:
Serreau Jovann
2026-04-02 22:16:50 +02:00
parent c7a1c1b39f
commit 28b84f09d4
5 changed files with 117 additions and 31 deletions

View File

@@ -272,6 +272,14 @@
job: "docker compose -f /var/www/crm-siteconseil/docker-compose-prod.yml exec -T php php bin/console app:dns:check --env=prod >> /var/log/crm-siteconseil-dns-check.log 2>&1"
user: bot
- name: Configure email tracking purge cron (daily at 5am)
cron:
name: "crm-siteconseil email-tracking purge"
minute: "0"
hour: "5"
job: "docker compose -f /var/www/crm-siteconseil/docker-compose-prod.yml exec -T php php bin/console app:email-tracking:purge --env=prod >> /var/log/crm-siteconseil-email-purge.log 2>&1"
user: bot
- name: Configure Cloudflare acme-challenge cleanup cron (daily at 6am)
cron:
name: "crm-siteconseil cloudflare clean"

View File

@@ -17,3 +17,6 @@ framework:
adapter: cache.adapter.redis
vite_cache_pool:
adapter: cache.adapter.redis
dns_infra_cache:
adapter: cache.adapter.redis
default_lifetime: 3600

View File

@@ -1,10 +1,15 @@
#!/bin/sh
echo "=== CRM SITECONSEIL Cron ==="
echo "Registered tasks:"
echo " - */5 * * * * app:orders:expire-pending"
echo " - 0 * * * * app:monitor:messenger"
echo " - 0 3 * * * app:meilisearch:check-consistency --fix"
echo " - 0 4 * * * app:attestations:clean"
echo " - */15 * * * app:services:check"
echo " - */5 * * * * app:orders:expire-pending"
echo " - 0 * * * * app:monitor:messenger"
echo " - 0 */2 * * app:dns:check"
echo " - 0 3 * * * app:meilisearch:check-consistency --fix"
echo " - 0 4 * * * app:attestations:clean"
echo " - 0 5 * * * app:email-tracking:purge"
echo " - 0 6 * * * app:cloudflare:clean"
echo " - 0 */6 * * app:stripe:sync"
echo " - */5 * * * * app:infra:snapshot"
echo " - */15 * * * * app:services:check"
echo "===================="
exec cron -f

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Command;
use App\Repository\EmailTrackingRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:email-tracking:purge',
description: 'Supprime les enregistrements EmailTracking de plus de 90 jours',
)]
class PurgeEmailTrackingCommand extends Command
{
public function __construct(
private EmailTrackingRepository $repository,
private EntityManagerInterface $em,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addOption('days', null, InputOption::VALUE_REQUIRED, 'Nombre de jours de retention', '90');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$days = (int) $input->getOption('days');
$threshold = new \DateTimeImmutable("-{$days} days");
$io->title("Purge EmailTracking (> $days jours)");
$io->text('Seuil : '.$threshold->format('d/m/Y H:i:s'));
$qb = $this->em->createQueryBuilder()
->delete('App\Entity\EmailTracking', 'e')
->where('e.sentAt < :threshold')
->setParameter('threshold', $threshold);
$deleted = $qb->getQuery()->execute();
$io->success("$deleted enregistrement(s) supprime(s).");
return Command::SUCCESS;
}
}

View File

@@ -10,6 +10,9 @@ use App\Service\MailcowService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class DnsReportController extends AbstractController
{
@@ -28,6 +31,7 @@ class DnsReportController extends AbstractController
CloudflareService $cloudflare,
MailcowService $mailcow,
EmailTrackingRepository $emailTrackingRepository,
#[Autowire(service: 'dns_infra_cache')] CacheInterface $cache,
): Response {
$tracking = $emailTrackingRepository->findOneBy(['messageId' => $token]);
@@ -35,43 +39,57 @@ class DnsReportController extends AbstractController
throw $this->createNotFoundException('Rapport introuvable.');
}
$errors = [];
$warnings = [];
$successes = [];
$domainResults = [];
$cacheKey = 'dns_infra_check_'.$token;
$cfRecordsByDomain = $this->loadCloudflareRecords($cloudflare);
$data = $cache->get($cacheKey, function (ItemInterface $item) use ($dnsCheck, $awsSes, $cloudflare, $mailcow): array {
$item->expiresAfter(3600); // 1 heure
foreach (self::DOMAINS as $domain) {
$checks = [];
$cfRecords = $cfRecordsByDomain[$domain] ?? [];
$errors = [];
$warnings = [];
$successes = [];
$domainResults = [];
$dnsCheck->checkSpf($domain, $checks, $errors, $warnings, $successes);
$this->enrichWithCloudflare($checks, $domain, 'SPF', 'TXT', $cfRecords);
$cfRecordsByDomain = $this->loadCloudflareRecords($cloudflare);
$dnsCheck->checkDmarc($domain, $checks, $errors, $successes);
$this->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords);
foreach (self::DOMAINS as $domain) {
$checks = [];
$cfRecords = $cfRecordsByDomain[$domain] ?? [];
$dnsCheck->checkMx($domain, self::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes);
$this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
$dnsCheck->checkSpf($domain, $checks, $errors, $warnings, $successes);
$this->enrichWithCloudflare($checks, $domain, 'SPF', 'TXT', $cfRecords);
$dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes);
$this->enrichLastCheck($checks, 'bounce.'.$domain, 'MX', $cfRecords);
$dnsCheck->checkDmarc($domain, $checks, $errors, $successes);
$this->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords);
$dnsCheck->checkWhois($domain, $checks, $errors, $warnings, $successes);
$dnsCheck->checkMx($domain, self::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes);
$this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
$this->checkAwsSes($domain, $awsSes, $dnsCheck, $checks, $errors, $successes, $cfRecords);
$this->checkMailcow($domain, $mailcow, $dnsCheck, $checks, $errors, $warnings, $successes, $cfRecords);
$dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes);
$this->enrichLastCheck($checks, 'bounce.'.$domain, 'MX', $cfRecords);
$domainResults[] = ['domain' => $domain, 'checks' => $checks];
}
$dnsCheck->checkWhois($domain, $checks, $errors, $warnings, $successes);
$this->checkAwsSes($domain, $awsSes, $dnsCheck, $checks, $errors, $successes, $cfRecords);
$this->checkMailcow($domain, $mailcow, $dnsCheck, $checks, $errors, $warnings, $successes, $cfRecords);
$domainResults[] = ['domain' => $domain, 'checks' => $checks];
}
return [
'domainResults' => $domainResults,
'errors' => $errors,
'warnings' => $warnings,
'successes' => $successes,
'date' => (new \DateTimeImmutable())->format('c'),
];
});
return $this->render('dns_report/index.html.twig', [
'domainResults' => $domainResults,
'errors' => $errors,
'warnings' => $warnings,
'successes' => $successes,
'date' => new \DateTimeImmutable(),
'domainResults' => $data['domainResults'],
'errors' => $data['errors'],
'warnings' => $data['warnings'],
'successes' => $data['successes'],
'date' => new \DateTimeImmutable($data['date']),
]);
}