Le crème vivait sur `<body>`, que Gmail supprime en réécrivant le message : la carte blanche se retrouvait collée au fond du client, blanc sur blanc chez les uns, bloc éblouissant sur fond noir chez les autres. Elle n'a plus de fond du tout — autour d'elle passe celui du lecteur, et c'est la bordure qui délimite, ce qui reste juste sur n'importe quel fond, y compris ceux qu'on ne connaît pas. Le mode sombre est déclaré (`color-scheme`) plutôt que subi : sans cette déclaration, Gmail et Apple inversent au jugé et le vert de marque en ressort délavé. Un piège qui ne se voyait qu'à l'écran : la marge intérieure était posée sur les `<table>`. En modèle de bordures fusionnées — celui de tout mail, et celui qu'Outlook applique de toute façon — la spécification CSS ignore la marge d'un élément `table`. Tout le message se collait aux bords. Elle est passée sur les `<td>`. Un châssis unique remplace les trois mises en page séparées. Le logo entre dans les mails, en image et non en texte, sur un bandeau qui reste sombre dans les deux modes : il n'a ainsi qu'une seule version à exister, juste partout. `public/` n'existait pas, et `output: "standalone"` ne le trace pas — sans la ligne ajoutée au Dockerfile le logo aurait répondu 404 chez tous les destinataires, panne invisible en développement. Les fiches promettaient 24 fois un geste qui n'existe pas — « si c'est déjà fait, dis-le-moi et je me tais », « dis-moi sa date d'expiration et je gère le tempo ». Le pied de page avait déjà été purgé de la même promesse ; le corps la répétait. Retirées sans remplacement : elles reviendront le jour où l'état utilisateur s'écrira. Le bloc d'accompagnement du rappel groupé disparaît aussi — « pour que tu n'aies pas à y penser toi-même » se démentait dans le seul message qui demande justement d'y penser. Et chaque échéance dont la fiche a une page publique porte désormais un lien vers elle : la bicéphalie ne servait que le SEO, alors que c'est au destinataire d'un rappel que l'explication entière manque le plus. Enfin, le lien d'activation. Il n'était pas un raccourci : sa vérification était la seule chose au monde capable de créer le compte de l'acheteur, better-auth ne le créant qu'à ce moment, `resolveMembership` n'inscrivant le premier membre que s'il y a une session, et `destinatairesFoyer` ne connaissant que les appartenances. Un mail lu le soir, un lien ouvert deux fois, un antispam qui le déclenche à vide, et un foyer payé 19,99 € restait sans compte, sans appartenance, donc sans destinataire de rappel, définitivement. Ce n'est pas un cas de bord : ce message n'est envoyé que quand personne n'était connecté avant de payer, donc l'unique mail d'activation qui existe était précisément celui dont l'expiration était fatale. Le webhook crée le compte et l'appartenance lui-même, depuis l'adresse que Stripe vient de collecter. Contrepartie assumée : Stripe ne prouve pas la possession de la boîte, et qui paierait pour l'adresse d'un tiers lui rattacherait un foyer — un ajout indésirable, jamais une prise de contrôle, et facturé à celui qui le tente. `npm run mails:echantillon` rend un exemplaire de chaque message depuis les vraies fiches, et, si une adresse est donnée, le rappel réel de ses foyers. D-048, D-049. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
35 lines
1.8 KiB
Docker
35 lines
1.8 KiB
Docker
FROM node:22-bookworm-slim AS builder
|
|
WORKDIR /app
|
|
COPY package*.json .npmrc ./
|
|
# `.npmrc` coupe tous les scripts d'installation (D-047) : on relance nommément les deux qui
|
|
# servent au build. `next build` lance ESLint, donc le résolveur d'imports doit être lié.
|
|
RUN npm ci && npm rebuild esbuild unrs-resolver
|
|
COPY . .
|
|
RUN npm run build
|
|
|
|
# Dépendances de production seules, pour le script de migration (audit E2) — le build standalone
|
|
# ne trace que ce que Next.js atteint depuis les pages/routes, pas les scripts hors app.
|
|
FROM node:22-bookworm-slim AS deps
|
|
WORKDIR /app
|
|
COPY package*.json .npmrc ./
|
|
# `sharp` est le seul paquet de production à avoir un script d'installation (optimisation des
|
|
# images de Next.js) ; il est relié à son binaire ici, les scripts étant coupés par défaut (D-047).
|
|
RUN npm ci --omit=dev && npm rebuild sharp
|
|
|
|
FROM node:22-bookworm-slim
|
|
WORKDIR /app
|
|
ENV NODE_ENV=production
|
|
COPY --from=builder /app/.next/standalone ./
|
|
COPY --from=builder /app/.next/static ./.next/static
|
|
# `output: "standalone"` ne trace ni `public/` ni `.next/static` : Next.js laisse les deux à
|
|
# copier à la main. Sans cette ligne, le logo des mails répondrait 404 chez tous les destinataires
|
|
# — une image cassée dans l'en-tête de chaque message, invisible en développement où le serveur
|
|
# sert `public/` depuis le disque.
|
|
COPY --from=builder /app/public ./public
|
|
COPY --from=builder /app/knowledge ./knowledge
|
|
COPY --from=builder /app/drizzle ./drizzle
|
|
COPY --from=builder /app/scripts/migrate.ts ./scripts/migrate.ts
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
EXPOSE 3000
|
|
# Migrations versionnées jouées avant le serveur (§ 27) — remplace le `drizzle-kit push` manuel.
|
|
CMD ["sh", "-c", "node --experimental-strip-types scripts/migrate.ts && node server.js"]
|