fokan/Dockerfile
Gautier Stefanini 3ad61a6878 Les mails posaient leur fond sur un <body> que Gmail retire, et le compte pendait à un lien de 15 minutes
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>
2026-08-05 21:04:27 +00:00

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"]