Le menu mobile (<nav class="mobile-nav">) omettait le lien « Service Commande »
sur about, application, cgv, contact, guide-envoi et tarifs — incoherent avec le
menu desktop et le footer (qui l'ont) et avec accueil (mobile complet). Insertion
du lien apres « Tarifs », identique aux autres entrees.
Constat issu de l'audit du site (revue qualite 2026-06-21).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
translations.js a Cache-Control max-age=7j sans versioning -> les visiteurs
gardaient l'ancien menu en cache. Cache-buster pour rendre le renommage
Contact->Inscription visible immédiatement par tous.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le nouveau flux reset (Edge Function mva-password-reset, deploye 2026-05-30)
envoie un lien ?token_hash=HASH&type=recovery au lieu de ?token=. La page-relais
lit desormais token_hash + type et construit le deep link
mvaglobalfret://reset-password?token_hash=...&type=... que l'app consomme via
verifyOtp({ token_hash, type }). Si token_hash est absent, on affiche un message
d'erreur (lien invalide) au lieu de tenter le redirect.
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>