La page declarait des sous-traitants faux/obsoletes (risque de conformite RGPD): retire HubSpot (decommissionne 2026-05-10) + son transfert hors-UE; Brevo -> Resend (USA) pour les emails; ajoute Cloudflare/Turnstile (USA, traite l'IP du visiteur, sous-traitant non declare); precise que la base de donnees (inscriptions/contacts) est hebergee par Hostinger (Allemagne, UE); corrige l'Article 6 transferts hors-UE en consequence. Applique aux 3 langues FR/EN/MG. Nettoie aussi les commentaires JS obsoletes (worker mva-hubspot-proxy decommissionne).
Verifs: 0 mention HubSpot/Brevo restante, node --check OK sur form-handler.js + confirmation.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le worker hubspot-proxy.js a ete decommissione le 2026-05-10 :
migration HubSpot CRM -> mva-api Fastify + Postgres + Resend (= mention
explicite dans js/form-handler.js).
Le dossier cloudflare-worker/ (DEPLOIEMENT.md + hubspot-proxy.js + wrangler.toml)
n'est plus utilise par le frontend mais traine dans le repo. Cleanup.
Refs:
- js/form-handler.js commentaire 'Migration 2026-05-10 : remplace l ancien
Cloudflare Worker mva-hubspot-proxy.sergemind4s.workers.dev (= decommissionne)
par les routes mva-api Fastify. La DB Postgres remplace HubSpot Contacts.'
- Audit hygiene M4S 2026-05-16
Bug compagnon de api PR #57 : crypto.randomBytes(48).toString hex = 96
caracteres, pas 64. La validation JS cote site rejetait tous les vrais
tokens avec 'Lien invalide ou incomplet'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compagnon de api PR #55 (bridge lead -> user).
Flow:
- Lead clique email setup compte
- Arrive sur /setup-password.html?token=XXX
- JS POST /auth/lookup-setup-token pre-remplit email + firstname + ref
- Form 2 password avec validation (min 8 majuscule chiffre)
- Submit POST /auth/setup-from-lead -> user cree + JWT
- Affiche success + lien vers application.html pour download app
Design: card centree, MVA gold/navy, mobile responsive, vanilla JS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le Cloudflare Worker hubspot-proxy était une solution temporaire car
le panel admin AdminJS n'était pas encore disponible. Maintenant qu'
AdminJS marche, on rapatrie tout le flow inscription côté MVA backend.
## Changements
- `js/form-handler.js`
- WORKER_PROXY_URL → API_BASE_URL = https://api.mva.mind4solutions.com
- checkExistingContact : POST /leads/check-email
(response shape : {exists, firstname, reference_client})
- setupContactForm : POST /leads/request-verification
- sendWelcomeBackEmail : POST /leads/welcome-back
- `js/confirmation.js`
- WORKER_PROXY_URL → API_BASE_URL
- POST /leads/verify-token (= au lieu de Worker action verifyToken)
- Detection token expiré/invalide via code INVALID_OR_EXPIRED
## Aucun changement HTML
Les forms HTML, IDs des éléments, validation côté client, gestion
Turnstile sont tous inchangés. Seules les URLs API changent.
## Côté backend (PR #44 mva-prestige-v2)
Les routes mva-api /leads/* sont déployées séparément avec :
- Validation Zod + Turnstile + rate limit
- DB Postgres (table leads + leads_pending) remplace HubSpot Contacts + KV
- Resend pour les emails (= unification écosystème M4S)
- AdminJS Resource leads pour Mélissa CRUD
## Cutover
Cette PR doit être merged + déployée APRÈS la migration backend
(PR #44) + après le run du script migrate-hubspot-to-postgres.js
(= les 4 contacts HubSpot existants en DB).
Le bridge HTML redirige maintenant vers le custom scheme natif
mvaglobalfret://reset-password?token=... au lieu de
https://auth.mind4solutions.com/reset-password.
Le flow Supabase (auth.m4s.com / Phase 2.1) ne sait pas valider les
tokens UUID custom émis par mva-api (Fastify). Sans ce fix, les emails
reset password Cluster A B2 atterriraient sur une page d'erreur
"Lien invalide".
UI fallback HTML conservée + bouton CTA stylé pour les cas où le
deep-link automatique ne déclenche pas l'app (browser desktop, app
non installée).
Fixes Cluster A B2 blocker (= session 2026-05-07 MVA app).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Create two new legal pages (FR/EN/MG) with LAATEL Corporation
company details (STAT, RCS, NIF). Add footer links to all pages
and translation keys for the three languages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The .btn class uses display:inline-flex with align-items:center but
no justify-content, so text stuck to the left when the parent flex
stretched the buttons to fill the row. Add justify-content:center
scoped to the confirmation card actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Direct CRM API needed crm.objects.contacts.write scope which the
existing Personal Access Key doesn't have. Using HubSpot Forms API
instead — same endpoint the browser used to call directly, but now
called server-side from the Worker after email verification.
This means HubSpot contact creation works without granting any
additional scopes to the token: anyone can submit a public form.
Also updates the welcome email warning to tell the customer to copy
the address EXACTLY as shown (the reference is now baked into line 1
of the address) and not to modify anything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the Forms API created the contact at form submission time —
which meant unverified signups (bots that pass Turnstile, typos, fake
emails) polluted HubSpot. Now:
- Form submit → Worker stores all data in KV (24h TTL) + sends Brevo
verification email (no HubSpot write)
- User clicks email link → Worker generates ref + creates HubSpot
contact via CRM API + sends welcome email with ref + Paris address
Plus this commit:
- Email header gets the MVA logo on the left of the dark blue banner
- Welcome email's first address line auto-injects (MVA-XXX) so the
customer can copy it directly onto their package
Also handles idempotency — clicking the verification link a second time
returns the existing ref without creating a duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resend requires a verified domain to send to arbitrary recipients —
mvaglobalfret.com isn't registered. Brevo accepts single-sender
verification on a free email address, so we can send from
mvaglobalfret@gmail.com without owning a domain.
- Worker: replace resendSend() with brevoSend() (api.brevo.com/v3/smtp/email)
- Env vars: BREVO_API_KEY, BREVO_SENDER_EMAIL, BREVO_SENDER_NAME
- Update comments in confirmation.js and form-handler.js
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architecture finale :
1. User remplit formulaire + passe Turnstile CAPTCHA → form-handler.js
2. form-handler.js POST au Worker avec action 'requestVerification'
3. Worker valide Turnstile, génère un token UUID, le stocke en KV (TTL 24h)
avec firstname/email/reference_client, puis envoie un email via Resend
avec un lien : confirmation.html?token=XXX
4. User reçoit email, clique 'Confirmer mon email'
5. confirmation.html lit le token de l'URL, POST au Worker avec action
'verifyToken'
6. Worker valide le token, envoie le welcome email via Resend (avec ref +
adresse Paris depuis env var), marque le token comme utilisé
7. confirmation.html affiche 'Inscription confirmée !'
Ainsi : ref + adresse Paris ne sortent JAMAIS avant validation email,
et les bots sont bloqués à l'étape 1 par Turnstile.
Setup Cloudflare requis (côté user) :
- RESEND_API_KEY : clé API Resend (re_...)
- RESEND_FROM : adresse expéditrice ('onboarding@resend.dev' pour test,
ou domain vérifié pour prod)
- SITE_URL : optionnel, défaut https://mva-global-fret.github.io/site-mva-global-fret
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- contact.html: ajout du widget Turnstile (site key: 0x4AAAAAADKDuc7Rmlb1svIL)
- form-handler.js: blocage de la soumission si pas de token Turnstile valide
- Worker: validation server-side du token via /turnstile/v0/siteverify
avant chaque appel sendWelcomeNow → bloque les bots qui n'auraient
pas passé le challenge côté client.
Le secret Turnstile est en env var Cloudflare (TURNSTILE_SECRET).
Limite humain : Turnstile détecte les bots avec très peu d'interaction
côté utilisateur (mode 'Managed', le plus souvent invisible).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug 1 — Ref MVA-001 dupliquée :
Le filtre HubSpot 'HAS_PROPERTY' avec value:'' retournait 0 résultats.
Suppression du value:'' → maintenant le worker liste correctement les
contacts avec reference_client et incrémente bien (testé : MVA-004).
Bug 2 — Email post-inscription jamais reçu :
Le double opt-in HubSpot ne se déclenche pas via Forms API sans
subscription consent (impossible à configurer sans nouveaux scopes
Private App). Pivot vers une approche plus simple :
- L'email de bienvenue est désormais envoyé directement après
soumission du formulaire (pas de DOI HubSpot)
- L'envoi passe par le Cloudflare Worker (action sendWelcomeNow)
pour que l'adresse Paris reste dans les env vars Cloudflare et
ne soit JAMAIS dans le JS public
- Worker appelle EmailJS REST avec firstname + reference + paris_address
Cleanup : message de succès reverti à 'Inscription réussie' (FR/EN/MG).
Anti-spam : protection légère via filtre email/téléphone côté formulaire.
La cron-based welcome (post-DOI) reste en place mais sera inerte tant
que aucun contact n'a le statut CONFIRMED côté HubSpot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant : layout row asymétrique, icône en coin haut-gauche, bouton outline
qui flotte, hiérarchie visuelle plate.
Maintenant :
- Carte centrée 560px max, padding plus respirant, bordure or 6px en haut
- Icône 84px navy/or avec drop-shadow → gros pop visuel
- Prix éclaté : '70 000' en gros chiffre, 'Ar / kg' en doré accent
- Description plus compacte, contenue à 440px
- Bouton primary (or solide) avec flèche animée au hover
- Variant mobile (<480px) avec tailles réduites
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architecture finale (Option A choisie) :
1. User submit form → contact créé en HubSpot avec reference_client
2. HubSpot envoie l'email de double opt-in (sans la ref ni l'adresse Paris)
3. User clique 'Confirmer' → HubSpot met hs_emailconfirmationstatus = CONFIRMED
4. Cron Cloudflare (toutes les 5 min) :
- Liste les contacts CONFIRMED + créés après le cutoff
- Filtre via Cloudflare KV (welcomed:<email>) pour idempotence
- Envoie le welcome email via EmailJS REST API avec :
• firstname
• reference_client
• paris_address (depuis env var PARIS_DEPOT_ADDRESS)
- Marque envoyé dans KV avec TTL 1 an
Protection :
- L'adresse du dépôt Paris ne quitte JAMAIS Cloudflare/EmailJS
- Elle n'arrive au client que dans le mail de bienvenue post-opt-in
- Bots qui n'ont pas un vrai email ne peuvent pas valider → ne reçoivent rien
- Anti-spam et anti-cartons-vides blindé
Ajout d'une action 'triggerWelcomeQueue' pour debug/manual run.
Doc complète dans cloudflare-worker/DEPLOIEMENT.md (étapes 1 à 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flux complet du double opt-in :
1. User soumet le formulaire → contact créé en HubSpot avec sa référence
2. HubSpot envoie un email 'Confirmez votre inscription'
3. User clique 'Confirmer' → HubSpot le marque 'subscribed'
4. HubSpot redirige vers confirmation.html?email=...
5. La page lit l'email, appelle le Worker Cloudflare pour récupérer la
référence du contact, et déclenche l'envoi de l'email de bienvenue
via EmailJS (avec la référence dedans)
6. Affiche succès + référence à l'écran
Idempotence via localStorage pour éviter de spammer l'email à chaque
rechargement de la page.
À configurer dans HubSpot Settings > Marketing > Email > Confirmation
d'inscription : URL de redirection après confirmation =
https://mva-global-fret.github.io/site-mva-global-fret/confirmation.html?email={{contact.email}}
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Retire le bloc 'Numéro de référence client' de la page de succès
- Met à jour le message en FR/EN/MG : 'Vous recevrez ensuite votre numéro
de référence client' après confirmation
- Désactive l'envoi immédiat de l'email EmailJS de bienvenue (qui
contenait déjà la référence). HubSpot envoie son email de
double opt-in qui sera customisé pour inclure la référence
via le token {{contact.reference_client}}.
Résultat : la référence n'est jamais visible avant que l'email ne soit
vérifié (puisque seuls les emails valides reçoivent le double opt-in).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HubSpot double opt-in is now enabled at the account level. After
submitting the form, contacts must click the confirmation link in
their email to be added to the marketing list.
The success message now explicitly tells the user to check their
inbox and click the confirmation link, instead of just saying
'inscription enregistrée'.
- title: 'Vérifiez votre boîte mail !' (FR), 'Check your inbox!' (EN), 'Jereo ny boaty mailaka!' (MG)
- main msg: focus on confirmation step
- icon: enveloppe-circle-check (gold) instead of generic green check
- note: nuance that the reference number is for tracking parcels
- emailSent: kept as is (informative footer)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Retire l'état caché (scale 0.05 / opacity 0 / offset -27vh)
- Retire les transitions de révélation (transform/opacity 2.2s)
- Retire l'animation de pulse halo (.cta-btn::after)
- Retire le déclenchement JS via la classe .revealed (l'avion qui croise le centre n'a plus d'effet sur le bouton)
Le bouton est désormais centré au milieu du viewport dès le chargement,
toujours cliquable. Hover conserve le scale 1.04 + shine effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant : flex row figé même sur mobile → l'icône 60px écrasait la colonne
texte → le bouton 'Voir nos tarifs détaillés' wrappait sur 2 lignes
avec un cadre asymétrique.
Maintenant : sous 600px de large, le layout passe en colonne (icône
au-dessus, contenu centré). Bouton avec white-space: nowrap pour rester
sur 1 ligne. Sur desktop (>=600px), le layout reste en row classique.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- overflow-y: auto pour scroll vertical quand le menu déborde
- height: 100dvh (dynamic viewport) pour meilleur rendu mobile (barre URL)
- -webkit-overflow-scrolling: touch (smooth iOS)
- overscroll-behavior: contain (pas de scroll-chain sur le body en dessous)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant: border-left 4px (juste à gauche).
Maintenant: border 2px tout autour pour bien encadrer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
J'utilisais --space-xxl qui n'existe pas dans le système (les vraies
variables sont --space-2xl/3xl). Du coup margin-top tombait à 0 et le
bloc collait au formulaire.
Corrigé : margin-top=96px (--space-3xl) + max-width réduit à 680px
pour un meilleur centrage visuel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avant : le bloc 'Rappel tarifaire' était collé en bas de la colonne droite,
créant un déséquilibre visuel avec le formulaire plus court à gauche.
Maintenant : sorti de la grille, centré avec max-width 720px sous les
deux colonnes, en grille 2 colonnes sur ses items pour une lecture
plus dense et équilibrée.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Retirer les icônes <i> minuscules à côté de chaque étape
- Ajouter le style CSS .steps-grid (grille 3 colonnes desktop, stack mobile)
- Ajouter le style .step-card (carte blanche, ombre, hover lift)
- Badge numéroté agrandi (64px) avec drop-shadow doré
- Ligne dorée discrète qui relie les 3 étapes en desktop
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Le mode legacy était bloqué en errored. On passe au build via Actions
(actions/deploy-pages@v4) — plus fiable et debug-able.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>