diff --git a/ansible/templates/caddy.j2 b/ansible/templates/caddy.j2
index 18601f3..de59716 100644
--- a/ansible/templates/caddy.j2
+++ b/ansible/templates/caddy.j2
@@ -20,7 +20,9 @@ intranet.ludikevent.fr, signature.ludikevent.fr, reservation.ludikevent.fr {
handle_path /utm_reserve.js {
redir https://tools-security.esy-web.dev/script.js
}
-
+ handle_path /ts.js {
+ redir https://widget.trustpilot.com/bootstrap/v5/tp.widget.bootstrap.min.js
+ }
# --- BLOC HEADER AVEC CSP ---
header {
X-Content-Type-Options "nosniff"
diff --git a/assets/reserve.js b/assets/reserve.js
index 6365def..7cc8674 100644
--- a/assets/reserve.js
+++ b/assets/reserve.js
@@ -2,6 +2,7 @@ import './reserve.scss';
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
import { CookieBanner } from "./tools/CookieBanner.js";
import * as Turbo from "@hotwired/turbo";
+import {onLCP, onINP, onCLS} from 'web-vitals';
// --- DÉTECTION BOT / PERFORMANCE ---
const isLighthouse = () => {
@@ -53,7 +54,6 @@ const initImageLoader = () => {
const images = mainContainer.querySelectorAll('img:not(.loaded)');
- console.log(images);
images.forEach(img => {
// Sécurité : si l'image est déjà chargée (cache), on marque et on skip
if (img.complete) {
@@ -195,8 +195,33 @@ const initRegisterLogic = () => {
updateSiretVisibility();
};
+const sendToAnalytics = ({ name, delta, id }) => {
+ // On ne veut pas polluer les stats avec les tests Lighthouse
+ if (isLighthouse()) return;
+
+ const body = JSON.stringify({
+ name, // 'LCP', 'INP', ou 'CLS'
+ value: delta, // La valeur de la mesure
+ id, // ID unique de la session de page (pour éviter les doublons)
+ path: window.location.pathname // Pour savoir quelle page est lente
+ });
+
+ const url = '/reservation/web-vitals';
+
+ // sendBeacon est idéal pour les stats car il ne bloque pas le thread principal
+ if (navigator.sendBeacon) {
+ navigator.sendBeacon(url, body);
+ } else {
+ fetch(url, { body, method: 'POST', keepalive: true });
+ }
+};
+
// --- INITIALISATION GLOBALE ---
document.addEventListener('DOMContentLoaded', () => {
+
+ onLCP(sendToAnalytics);
+ onINP(sendToAnalytics);
+ onCLS(sendToAnalytics);
initLoader();
initImageLoader();
// Enregistrement Custom Elements
diff --git a/migrations/Version20260127223021.php b/migrations/Version20260127223021.php
new file mode 100644
index 0000000..8cf97d8
--- /dev/null
+++ b/migrations/Version20260127223021.php
@@ -0,0 +1,33 @@
+addSql('CREATE TABLE site_performance (id SERIAL NOT NULL, name VARCHAR(255) NOT NULL, value DOUBLE PRECISION NOT NULL, path VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
+ $this->addSql('COMMENT ON COLUMN site_performance.created_at IS \'(DC2Type:datetime_immutable)\'');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('CREATE SCHEMA public');
+ $this->addSql('DROP TABLE site_performance');
+ }
+}
diff --git a/migrations/Version20260127223100.php b/migrations/Version20260127223100.php
new file mode 100644
index 0000000..3234cc0
--- /dev/null
+++ b/migrations/Version20260127223100.php
@@ -0,0 +1,32 @@
+addSql('ALTER TABLE site_performance ADD metric_id VARCHAR(50) DEFAULT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('CREATE SCHEMA public');
+ $this->addSql('ALTER TABLE site_performance DROP metric_id');
+ }
+}
diff --git a/package.json b/package.json
index af1ed6d..5535919 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"tailwindcss": "^4.1.18",
"tom-select": "^2.4.3",
"vite-plugin-compression": "^0.5.1",
- "vite-plugin-favicon": "^1.0.8"
+ "vite-plugin-favicon": "^1.0.8",
+ "web-vitals": "^5.1.0"
}
}
diff --git a/src/Controller/ReserverController.php b/src/Controller/ReserverController.php
index fc967c4..314407c 100644
--- a/src/Controller/ReserverController.php
+++ b/src/Controller/ReserverController.php
@@ -7,6 +7,7 @@ use App\Entity\AccountResetPasswordRequest;
use App\Entity\Customer;
use App\Entity\CustomerTracking;
use App\Entity\Product;
+use App\Entity\SitePerformance;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Logger\AppLogger;
@@ -62,6 +63,35 @@ class ReserverController extends AbstractController
'products' => $products
]);
}
+ #[Route('/reservation/web-vitals', name: 'reservation_web-vitals', methods: ['POST'])]
+ public function webVitals(Request $request, EntityManagerInterface $em): Response
+ {
+ $data = json_decode($request->getContent(), true);
+
+ if (!$data || !isset($data['name'], $data['value'])) {
+ return new Response('Invalid data', Response::HTTP_BAD_REQUEST);
+ }
+
+ // On vérifie si cet ID de métrique existe déjà pour éviter les doublons
+ // (web-vitals peut renvoyer plusieurs fois la même métrique si elle s'affine)
+ $existing = $em->getRepository(SitePerformance::class)->findOneBy(['metricId' => $data['id']]);
+
+ $perf = $existing ?? new SitePerformance();
+
+ $perf->setName($data['name']);
+ $perf->setValue((float)$data['value']);
+ $perf->setPath($data['path'] ?? '/');
+ $perf->setMetricId($data['id'] ?? null);
+ $perf->setUpdatedAt(new \DateTime());
+
+ if (!$existing) {
+ $em->persist($perf);
+ }
+
+ $em->flush();
+
+ return new Response('', Response::HTTP_NO_CONTENT);
+ }
#[Route('/reservation/umami', name: 'reservation_umami', methods: ['POST'])]
public function umami(
diff --git a/src/Entity/SitePerformance.php b/src/Entity/SitePerformance.php
new file mode 100644
index 0000000..1158d4d
--- /dev/null
+++ b/src/Entity/SitePerformance.php
@@ -0,0 +1,95 @@
+id;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): static
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getValue(): ?float
+ {
+ return $this->value;
+ }
+
+ public function setValue(float $value): static
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ public function getPath(): ?string
+ {
+ return $this->path;
+ }
+
+ public function setPath(string $path): static
+ {
+ $this->path = $path;
+
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeImmutable $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+
+ return $this;
+ }
+
+ public function getMetricId(): ?string
+ {
+ return $this->metricId;
+ }
+
+ public function setMetricId(?string $metricId): static
+ {
+ $this->metricId = $metricId;
+
+ return $this;
+ }
+}
diff --git a/src/Repository/SitePerformanceRepository.php b/src/Repository/SitePerformanceRepository.php
new file mode 100644
index 0000000..898ca2a
--- /dev/null
+++ b/src/Repository/SitePerformanceRepository.php
@@ -0,0 +1,43 @@
+
+ */
+class SitePerformanceRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, SitePerformance::class);
+ }
+
+ // /**
+ // * @return SitePerformance[] Returns an array of SitePerformance objects
+ // */
+ // public function findByExampleField($value): array
+ // {
+ // return $this->createQueryBuilder('s')
+ // ->andWhere('s.exampleField = :val')
+ // ->setParameter('val', $value)
+ // ->orderBy('s.id', 'ASC')
+ // ->setMaxResults(10)
+ // ->getQuery()
+ // ->getResult()
+ // ;
+ // }
+
+ // public function findOneBySomeField($value): ?SitePerformance
+ // {
+ // return $this->createQueryBuilder('s')
+ // ->andWhere('s.exampleField = :val')
+ // ->setParameter('val', $value)
+ // ->getQuery()
+ // ->getOneOrNullResult()
+ // ;
+ // }
+}
diff --git a/templates/revervation/base.twig b/templates/revervation/base.twig
index 2e96f25..95f739c 100644
--- a/templates/revervation/base.twig
+++ b/templates/revervation/base.twig
@@ -75,6 +75,7 @@
{% endif %}
+
{{ vite_asset('reserve.js',{}) }}
{% block stylesheets %}{% endblock %}