Skip to content

docs(deployment): version skew, clé Server Actions et accès direct à l'origine - #110

Open
TheSawkit wants to merge 99 commits into
mainfrom
dev
Open

docs(deployment): version skew, clé Server Actions et accès direct à l'origine#110
TheSawkit wants to merge 99 commits into
mainfrom
dev

Conversation

@TheSawkit

Copy link
Copy Markdown
Owner

No description provided.

TheSawkit and others added 30 commits July 20, 2026 22:59
Un titre non encore sorti — ou obscur — a vote_average = 0, ce qui affichait un
badge doré « N/A » à étoile. Trompeur : il suggère une note là où il n'y en a pas.

Le badge n'est désormais rendu que si vote_average > 0, aussi bien sur les cartes
(MediaCard) que sur la bannière détail (MediaBanner). Décision prise côté serveur
à partir de la donnée, donc correcte dès le premier rendu — pas de dépendance à
l'hydratation client (une tentative via useIsUnreleased ne mettait pas à jour le
badge dans le shell prérendu par Cache Components).

Vérifié dans le navigateur sur un build standalone de production : « The End of
Oak Street » (sortie août 2026, note 0) n'affiche plus de badge ; « Fight Club »
affiche toujours 8.4.
…earchDropdown

- Fin du doublon xl/2xl : sm 4 · md 8 · lg 12 · xl 16 · 2xl 24, le 3xl disparaît
  (rounded-2xl→xl, rounded-3xl→2xl — zéro changement visuel)
- Tokens sémantiques --radius-banner (22px) et --radius-app-icon (22.5%) remplacent
  les derniers rounded-[Npx] arbitraires (BottomTabBar, SpotlightPick, icône PWA)
- TiltCard prend un radius token string comme GlowBorder, plus jamais un nombre
- SearchDropdown passe sur glass-popover : lisible sur tout contenu, dans les 2 thèmes
- tooltip.tsx référençait des tokens inexistants (bg-foreground) et n'était monté
  nulle part hors du provider
- TooltipProvider retiré de app/providers.tsx, @radix-ui/react-tooltip désinstallé
- TMDB : timeout 10s, erreurs réseau (DNS/reset/hang) retournées en data au lieu de
  traverser la frontière use cache et tuer le rendu ; 404 d'ids morts cachés 1h
- Watchmode : timeout 8s + cooldown global 5 min après 429/401/402 — plus un seul
  appel dépensé pendant l'épuisement du quota mensuel
- iTunes lookup (icônes providers) : timeout 5s
- lib/monotonic-now.ts : horloge monotone pour les durées — Date.now() pendant un
  prerender Cache Components avorte le shell en 500
- reportSwallowed envoie chaque fallback avalé à Bugsink en warning, dédupliqué
  5 min par label+message (horloge monotone, safe en prerender)
- ErrorCard capture les erreurs client sans digest (les erreurs serveur arrivent
  déjà via onRequestError) — les 12 error.tsx transmettent désormais l'objet error
- global-error.tsx capture les crashs de shell
- Le profil chargeait la watchlist avec .limit(1000) : au-delà, compteurs et grilles
  faux (2074 entrées réelles → 299/699 affichés) ; passage sur fetchAllRows,
  vérifié en rendu réel : 471 à voir / 1601 vus, conformes au SQL
- Même correction : épisodes du continue watching, progression d'une série,
  notes utilisateur, amis, demandes en attente, playlists (caps 100 arbitraires)
- Le badge Avis affiche un vrai count SQL au lieu de la taille de la première page
- Les chips posées sur les vignettes (badge S/E, Marquer vu, note, statut)
  mélangeaient scrim sombre fixe et texte thémé — illisibles en light
- Passage sur le matériau surface opaque (bg-surface + border-border) :
  blanc en light, noir en dark, automatique, sans blur (interdit en répété)
- Badge note des posters : text-rating-gold (or vif en dark, ambre en light)
- Scrims à texte blanc sur image (play trailers, X playlist) conservés tels quels
- MediaCard et SeasonCard : pointer-events-none sur les overlays masqués,
  réactivés au survol — opacity-0 ne coupe pas les événements pointeur et
  Tailwind v4 borne hover: à @media (hover:hover)
- corrige deux mutations involontaires en PWA : un tap au bas d'une card
  ajoutait le titre à la watchlist, un tap sur une card de saison marquait
  la saison entière comme vue
- tests : helper pageButton — watchlist/list-controls/resilience ciblaient
  un bouton de card au lieu de la page détail
- tests : garde anti-régression touch-ghost-controls (aucun contrôle
  invisible ne doit être tapable)
- shadow-control-lift porte un highlight inset à 0.7 en clair (0.18 en sombre) :
  sur un fond rouge translucide il dessine un contour blanc détaché
- WatchButton, EpisodeWatchButton, SeasonWatchButton et la pastille
  « Vu » du dashboard passent à shadow-card-sm
- le token reste inchangé pour son usage sur fond opaque (StreamingSettings)
Le slot est monté dans le layout, donc rendu sur chaque page : sur /dashboard
il déclenchait cinq requêtes dont trois doublons (états des prompts et compte
watchlist demandés deux fois, plateformes hors du cache déjà utilisé par
buildLibrarySections).

- lib/data/prompts.ts : getCachedPromptStates + getCachedWatchlistCount,
  même pattern React.cache que lib/data/watchlist.ts
- StreamingPromptSection passe sur getCachedStreamingProviders
- le compte reste un count exact plutôt que la liste complète, puisque le slot
  s'affiche aussi sur les pages qui n'ont pas besoin de la watchlist

Cinq requêtes ramenées à deux par rendu du dashboard.
- countVisit() était évalué derrière un court-circuit `canImport &&`, donc le
  compteur de visites n'avançait que pour les comptes à watchlist vide. Il est
  maintenant lu en tête d'effet, indépendamment de qui s'en sert.
- le bandeau fermé restait annoncé par les lecteurs d'écran et focusable :
  translate-y ne le sort que visuellement, d'où aria-hidden + inert.
- isPromptKey / isPromptState remplacent les `as` posés avant vérification sur
  les lignes user_prompts.
- usePWAInstall annule son timer avant d'en reposer un, si le navigateur
  réémet beforeinstallprompt.
Les endpoints recommendations et similar répondent 404 pour les titres de
niche, et un id disparaît quand TMDB fusionne ou supprime une fiche. Chaque
dashboard poussait donc plusieurs warnings dans Bugsink pour une absence que
tous les appelants gèrent déjà par un fallback.

TMDBNotFoundError, sans dépendance pour rester importable côté client, permet
à reportSwallowed de la traiter en console.debug. Toute autre erreur (réseau,
5xx, quota) garde son warning et sa remontée.
Le dashboard affichait « Tes statistiques » juste au-dessus de « Vos prochains
visionnages » : settings tutoyait déjà, le reste vouvoyait. 93 chaînes alignées
sur le tutoiement, y compris les impératifs pluriels (« Découvrez » → « Découvre »)
que le premier passage sur vous/votre/vos avait laissés.

app/manifest.ts duplique quatre descriptions de metadata.ts et suit le même
changement.

Concerne aussi /terms et /privacy : « Tes droits », « Ton compte ».
Le tutoiement généralisé avait emporté les pages légales, où le vouvoiement
reste la convention. Seul le bloc legal français revient en arrière : le
dashboard, l'explorer et le reste de pages.ts gardent le tutoiement.
Deux défauts distincts rendaient la watchlist inaccessible sur mobile :

- le dégradé décoratif de MediaCard couvre toute la carte en `inset-0` sans
  `pointer-events-none`, donc il absorbait les taps destinés aux boutons placés
  au-dessus de lui — y compris quand ceux-ci étaient parfaitement visibles.
- les contrôles révélés au survol restaient inertes sur tactile, où `hover:` ne
  se déclenche jamais : sans être ni visibles ni atteignables, le bouton
  watchlist d'une card et l'icône de saison non commencée n'avaient aucun
  équivalent au doigt.

`pointer-coarse:` (Tailwind 4.1+) les affiche et les active sur tactile, ce qui
lève l'alternative « invisible mais tapable » / « inatteignable » sans toucher
au comportement desktop.

Vérifié sur dashboard, library, /movie/[id] et /tv/[id] en viewport 390×844 :
plus aucun bouton dont le tap n'atteint pas sa cible. touch-ghost-controls et
media-card-tap restent verts.
Annule l'affichage permanent introduit par cdb111d : sur une card, l'overlay
de survol doit rester invisible et inerte au doigt, pas occuper l'affiche.

Seul `pointer-events-none` est conservé sur le dégradé décoratif de MediaCard :
il couvre la carte en `inset-0` et n'a aucune raison d'être une cible de
pointeur, même si ce n'était pas lui qui bloquait les boutons.
Next bloque `/_next/*` en cross-origin depuis Next 15.2 : tester depuis un
téléphone sur l'IP du Mac faisait rejeter le HMR, les payloads RSC et les
Server Actions. L'app se chargeait mais paraissait morte — chaque lien
repassait en navigation pleine page (donc sans barre de progression) et aucun
bouton ne réagissait, sans la moindre erreur visible côté client.

`allowedDevOrigins` reçoit les IPv4 non internes de la machine, résolues au
chargement de la config plutôt qu'écrites en dur puisque l'adresse suit le
réseau. Sans effet en production, où l'option est ignorée.

Vérifié en WebKit (le moteur de Safari) sur iPhone 13 émulé, via l'IP :
navigation client rétablie et barre de progression de retour.
`navigator.serviceWorker.ready` n'est jamais résolue tant qu'aucun worker n'est
activé. Le serveur de dev ne génère pas `sw.js` : le statut restait donc sur
« loading » indéfiniment, ce qui masquait le CTA d'activation (il exige `off`)
et laissait le réglage des notifications inutilisable. Même impasse en
production si l'enregistrement du worker échoue.

`activeServiceWorker()` interroge d'abord `getRegistration()`, qui répond tout
de suite, puis borne l'attente de `ready`. Sans worker, le statut devient
`unsupported` — refus honnête plutôt qu'attente sans fin.
`isSettingsTab` vivait dans SettingsNav, marqué `'use client'`, et la page est
un Server Component : Next refuse l'appel — « Attempted to call isSettingsTab()
from the server » — et toute la page tombait sur ErrorCard. Introduit avec le
deep-link `?section=` des CTA, invisible au build puisque l'erreur est à
l'exécution, et non couvert : aucun test ne visitait /settings.

Le tableau d'onglets et son garde passent dans `components/settings/tabs.ts`,
sans directive, importable des deux côtés de la frontière.

Ajoute 8 cas E2E sur /settings, dont chaque `?section=` visée par un CTA et une
valeur inconnue, pour que la page ne puisse plus casser en silence.
Il était le seul des quatre à ne pas passer par PromptBanner : une carte pleine
largeur posée dans le flux du dashboard, là où les trois autres partagent le
bandeau sous la navbar. Deux styles pour un même rôle.

- `streaming` entre dans BANNER_PRIORITY, en dernier
- PromptSlot calcule son éligibilité côté serveur, en réutilisant le cache de
  getCachedStreamingProviders déjà lu par buildLibrarySections
- SETTINGS_TARGET remplace l'URL de réglages codée en dur dans le gestionnaire
  d'action, chaque prompt déclarant l'onglet qu'il ouvre
- StreamingPromptCard et StreamingPromptSection disparaissent

Conséquence assumée : le services tombe sous la règle commune d'un seul CTA
tous les trois jours, donc il s'affichera plus rarement qu'avant, où il vivait
hors de la file. Import et services restent exclusifs de fait (moins de 3
titres contre 5 au moins).

Vérifié en rendu réel : bandeau à y=64 avec l'icône téléviseur, carte inline
absente, et le cas du moteur qui affirmait l'inverse est mis à jour.
TheSawkit and others added 30 commits July 28, 2026 06:35
`disabled={isPending}` n'atteint le DOM qu'au rendu suivant : entre le clic et
ce rendu, un second clic passe et rejoue la Server Action. C'est ce qui produisait
les demandes d'ami en double — la contrainte d'unicité rejetait bien la seconde
insertion, mais le quota horaire était consommé et un toast d'erreur s'affichait.
Là où rien ne contraint la table, la duplication était réelle : deux clics sur
« créer une playlist » créaient deux playlists, et un double clic sur l'import
rejouait toute la bibliothèque.

`createInFlightGuard` sérialise les appels et abandonne le ré-entrant ; le hook
`useGuardedTransition` l'expose avec la signature de useTransition, pose le drapeau
de façon synchrone (startTransition exécute son scope immédiatement) et le relâche
même si l'action échoue. Les onze composants qui mutaient derrière une garde par
rendu y passent : amis, invitations, playlists, avis, notes d'épisode, passkeys,
déconnexion, export et import de données.

Laissés tels quels après vérification : useAsyncAction/useOptimisticAction ont déjà
un verrou synchrone, et InfiniteScrollMedia déduplique par clé — au pire une requête
de trop, jamais une carte en double.
Le cadre faisait 1181x224 en 1181 de large, soit un 5.27/1 : le backdrop TMDB
étant un 16/9, les deux tiers de l'image étaient rognés et il n'en restait
qu'une bande. Le cadre passe en 16/9 sur mobile et 21/9 au-delà — mesuré à
1181x506, ratio 2.33 — via les tokens `--aspect-cinema` et `--aspect-hero`,
et le cadrage repasse au centre, là où les backdrops sont composés.

`Aurora` posait deux halos `--color-red-rgb` floutés à 46px par-dessus l'affiche :
ce sont eux qui teintaient la bannière. Retiré du bandeau ; les autres surfaces
qui l'utilisent (accueil, profil, bento) ne sont pas touchées. La lisibilité du
texte repose désormais sur deux dégradés noirs, vertical et latéral, et le bloc
est ancré en bas comme sur une jaquette de service de streaming.

Les vignettes des rangées demandaient `w185` pour un rendu à 198 px CSS, soit
396 px sur un écran 2x : elles étaient affichées au double de leur définition.
Toutes les cartes passent en `w342`.
Les deux gestes partaient tout seuls pendant le défilement en PWA. Le verrou
passe par `touch-action: pan-x pan-y` sur html et body : les deux axes de
défilement restent libres, les gestes de zoom disparaissent. `user-scalable=no`
n'est pas utilisé — iOS l'ignore depuis la version 10 et il ne servirait qu'à
faire rougir l'audit d'accessibilité.

WebKit zoome le viewport en dehors de touch-action, donc CSS seul laisse le
pincement actif sur iPhone : PreventPinchZoom neutralise les événements
gesturestart/change/end, non standard mais seul levier disponible là-bas.

Compromis assumé et demandé : le critère WCAG 1.4.4 n'est plus rempli, les
malvoyants perdent l'agrandissement natif. Le commentaire du viewport disait
l'inverse, il est corrigé pour ne pas mentir à la prochaine lecture.

Trois tests E2E figent la décision : touch-action effectif sur html/body,
absence de user-scalable dans le viewport, et manipulation conservé sur les
éléments interactifs.
Le champ acteur des listes (bibliothèque, watchlist de profil, playlists) était un
texte libre : une faute d'orthographe rendait zéro résultat sans rien dire, et il
fallait connaître la graphie exacte du nom pour filtrer quoi que ce soit.

Une liste de suggestions TMDB s'ouvre sous le champ dès deux caractères, servie par
`/api/search?type=person` — la route existante, donc le rate-limit du proxy (30/min)
et le cache CDN couvrent déjà ces appels. Chaque entrée montre la photo et les deux
titres les plus connus de la personne, de quoi départager les homonymes.

Sélectionner une suggestion verrouille le filtre sur l'ID TMDB : `filterListByActor`
compare alors `member.id` au lieu du nom normalisé, ce qui supprime les confusions
entre homonymes et les variantes de graphie selon la langue. Le verrou est relâché
dès que le texte est retapé (`withActorQuery` remet `actorId` à null), sinon on
filtrerait sur l'ancien acteur avec un nouveau texte affiché. La sélection saute le
debounce de 400 ms, elle n'a rien à attendre.

L'état persiste en localStorage comme le reste des contrôles de liste, et quatre
tests figent les transitions acteur : verrou, relâchement, effacement, et
non-régression sur le tri et les genres.
…atars

Les photos Google ne s'affichaient plus nulle part. Ce n'est pas la config des
images — `lh3.googleusercontent.com` figure déjà dans `remotePatterns` et dans la
CSP `img-src` — c'est le `Referer` : notre en-tête `Referrer-Policy` envoie l'origine
avec chaque requête et Google coupe la connexion sur les avatars hotlinkés
(`transferSize: 0`, aucune réponse HTTP). Mesuré sur la même URL, mêmes conditions :
0/6 chargements avec la politique par défaut, 6/6 avec `no-referrer`. En curl la
requête passait, faute d'en-têtes `Sec-Fetch-*`, d'où l'illusion d'une URL saine.

Les cinq rendus d'avatar passent donc en `referrerPolicy="no-referrer"`, sans effet
sur Supabase, DiceBear et les blobs locaux qui ne filtrent pas là-dessus.

Aucun d'eux n'avait de `onError` : une URL morte laissait un carré vide au lieu des
initiales. `useBrokenImage` mémorise la source qui a échoué — la clé est la source
elle-même, donc un avatar remplacé est réessayé au lieu de rester cassé.

Enfin la photo du provider est copiée dans le bucket `avatars` à la première connexion
OAuth, derrière `after()` pour ne rien ajouter à la latence du login : le rendu ne
dépend plus d'un CDN tiers et l'IP des visiteurs ne part plus chez Google à chaque
affichage. Un avatar déjà uploadé n'est jamais écrasé. `user_metadata` étant
modifiable par l'utilisateur, l'URL source est filtrée avant le fetch — https et hôte
`lh<n>.googleusercontent.com` uniquement, sans quoi la copie serait un SSRF ouvert.
- PodDisruptionBudget maxUnavailable: 1 — correct quelle que soit la taille
  du ReplicaSet, contrairement à minAvailable qui autoriserait l'éviction de
  9 pods sur 10
- topologySpreadConstraints en ScheduleAnyway : répartit sans jamais bloquer
  le scheduling quand le pool n'a qu'un nœud disponible
- requests 500m/1Gi (au lieu de 250m/512Mi) : le HPA cible un pourcentage de
  la request, 75% de 250m déclenchait un scale-up permanent qui entraînait le
  node pool dans le même va-et-vient
- ephemeral-storage borné : un pod ne peut plus déclencher un DiskPressure
  qui évincerait tous les pods du nœud (20 Go seulement)
- NODE_OPTIONS borne le heap V8 à 768 Mo sous la limite de 1 Gi
- startupProbe (60 s) remplace les initialDelaySeconds : la liveness ne peut
  plus tuer un pod avant qu'il soit prêt
- spec.replicas retiré — le champ appartient au HPA
- image en placeholder __IMAGE_TAG__ : un apply brut échoue visiblement au
  lieu de reverter le cluster sur :latest
- ingress affinity-mode: balanced — persistent gardait un client collé à un
  pod disparu

Risque : le premier apply peut brièvement ramener le Deployment à 1 pod si la
migration vers server-side apply ne préserve pas le champ replicas. Vérifier
`kubectl -n reelmark get hpa` après le premier déploiement.
- deploy.yml passe de `kubectl set image` à `kubectl apply --server-side` :
  les resources, probes, PDB et HPA partent en prod avec le commit qui les
  contient. Le mode server-side est requis, pas cosmétique — en client-side
  l'absence de spec.replicas dans le manifeste le remettrait à son défaut
  (1 pod) à chaque déploiement au lieu de le laisser au HPA
- infra.yml (nouveau) gère ingress-nginx via Helm et k8s/ingress.yaml, sur
  changement des fichiers edge ou en workflow_dispatch — l'edge ne bouge pas
  à chaque push applicatif
- ingress-nginx passe à 2 replicas avec anti-affinité `required` sur hostname :
  les deux pods ne peuvent pas partager un nœud, ce qui interdit au cluster
  autoscaler Infomaniak (1→10, non configurable) de redescendre à 1 nœud. Le
  plancher de 2 nœuds devient structurel — c'est le correctif des coupures
  Cloudflare 1016/523 et des 404 « default backend »
- chart pinné à 4.15.1 : une version flottante ferait passer l'edge en majeure
  sans revue au premier déclenchement
- concurrency group sur les deux workflows, permissions minimales sur infra
Un secret GitHub vide produisait un bundle client avec des valeurs undefined
pour Supabase, sans aucune erreur au build — l'app partait en prod cassée.
Même garde que celle déjà en place sur tmdb_token, sur les trois variables
sans lesquelles le client est inutilisable.

.dockerignore exclut docs, context-ai et tsconfig.tsbuildinfo (645 Ko) qui
invalidaient le cache de build à chaque modification.
- plancher de 2 nœuds imposé par l'anti-affinité ingress, l'autoscaling du
  node pool Infomaniak (1→10) n'étant pas configurable
- dimensionnement détaillé pour a2-ram4-disk20-perf1 (2 pods par nœud)
- deux workflows, deux rythmes : app à chaque push, edge sur demande
- raison du server-side apply
- Cache Rule Cloudflare sur /_next/static — sort les assets des pods et fait
  tomber la charge CPU qui déclenche le HPA
- section Scaling corrigée (disait 2→5 à 70%, le manifeste dit 2→10)
- runAsNonRoot en uid/gid 1001 (l'utilisateur créé par le Dockerfile),
  seccompProfile RuntimeDefault, allowPrivilegeEscalation: false, toutes les
  capabilities retirées
- readOnlyRootFilesystem: true, avec deux emptyDir bornés sur les seuls
  chemins que le serveur standalone écrit à l'exécution — /app/.next/cache et
  /tmp. Le fsGroup: 1001 du pod est ce qui les rend inscriptibles : sans lui
  un emptyDir reste root:root en 0755 et le conteneur non-root échoue au
  premier write de cache
- automountServiceAccountToken: false — l'app ne parle jamais à l'API
  Kubernetes, le token monté par défaut est une surface d'attaque offerte
- timeoutSeconds explicites sur les trois probes. Le défaut de 1 s suffit à
  faire redémarrer un pod dont l'event loop est simplement occupé à rendre du
  SSR, et la charge se reporte alors sur les survivants — la panne en cascade
  classique de la liveness probe
- terminationGracePeriodSeconds 30 → 45. Next termine les requêtes en vol et
  les callbacks after() avant de sortir sur SIGTERM et demande 10 à 30 s de
  drain ; toutes les Server Actions du projet déferrent leur revalidation
  dans after()

Réf: kubernetes.io/docs/concepts/security/pod-security-standards
     nextjs.org/docs/app/guides/self-hosting
     blog.colinbreck.com/kubernetes-liveness-and-readiness-probes
nginx bufferise les réponses upstream par défaut : il attend la réponse
complète avant de l'envoyer au client, ce qui annule le streaming des sections
<Suspense> des fiches détail. La bannière ne peignait plus avant que les
données Supabase soient arrivées, alors que les pages sont précisément
construites pour ne bloquer que sur le trio TMDB.

Réf: nextjs.org/docs/app/guides/self-hosting#streaming-and-suspense
Trois flux seulement : entrée depuis ingress-nginx sur 3000, DNS vers
kube-system, HTTPS sortant hors plages RFC1918. Un pod compromis ne peut donc
ni scanner le cluster ni joindre l'API server.

Volontairement hors CI : une erreur de périmètre coupe l'app sans message
clair. La procédure de pose et de retour arrière est dans DEPLOYMENT.md, avec
le point à vérifier au préalable (Bugsink joignable en IP publique).
- profil restricted, chemins inscriptibles et rôle du fsGroup
- drain SIGTERM aligné sur les callbacks after()
- seuils de probes et raison du timeoutSeconds explicite
- section 7 bis : pose et retrait de la NetworkPolicy
- note streaming (proxy-buffering)

Sources officielles liées en bas de section.
- NEXT_DEPLOYMENT_ID reçoit le SHA du commit. Next suffixe alors les assets en
  `?dpl=<sha>` et compare l'identifiant du client à celui du serveur : en cas
  d'écart il déclenche un rechargement complet au lieu de demander un chunk
  que le pod ne sert plus. C'est le correctif de fond des 404 pendant un
  rolling update — l'affinité par cookie n'en réduisait que la probabilité
- NEXT_SERVER_ACTIONS_ENCRYPTION_KEY par secret BuildKit, optionnel avec
  avertissement. Sans clé fixe, chaque build tire la sienne et une Server
  Action chiffrée par l'ancien pod échoue sur le nouveau (« Failed to find
  Server Action ») pendant toute la fenêtre du déploiement

Réf: nextjs.org/docs/app/api-reference/config/next-config-js/deploymentId
     nextjs.org/docs/app/guides/self-hosting#multi-server-deployments
- HPA behavior : sans lui le contrôleur double le nombre de pods toutes les
  15 s. Chaque paire de pods réclame un nœud au cluster autoscaler, qui met 1
  à 3 min à le provisionner — la montée brutale commande des machines dont le
  pic n'a plus besoin quand elles arrivent. Fenêtre de 60 s et +2 pods/min à
  la montée, 50%/min à la descente
- PDB unhealthyPodEvictionPolicy: AlwaysAllow — un pod déjà cassé
  immobilisait le budget et bloquait le drain d'un nœud, alors que l'évincer
  restaure la disponibilité au lieu de la dégrader
…l'origine

- génération et rôle de NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
- effet de NEXT_DEPLOYMENT_ID sur les 404 de rolling update
- section 7 ter : le LB Octavia est joignable sans passer par Cloudflare, donc
  sans WAF ni rate limiting — et cf-connecting-ip est alors forgeable. Fix par
  whitelist des plages Cloudflare, avec le prérequis à vérifier d'abord (le
  SNAT d'Octavia peut masquer l'IP source et faire tout rejeter)
Le fichier affirmait que le resize/format était délégué à Cloudflare Polish.
C'est faux : avec images.unoptimized, next/image émet un src pointant sur
image.tmdb.org, un domaine tiers. Les posters ne traversent jamais le proxy
Cloudflare du domaine, et Polish ne couvre que les images servies par
l'origine.

L'état actuel reste le bon compromis à ce dimensionnement — la note explique
désormais pourquoi, et ce que coûteraient les deux alternatives.
Le namespace reelmark est créé par k8s/app.yaml, donc par le workflow
applicatif. Sur un cluster vierge, lancer infra.yml en premier — ce que la
doc recommandait pour que le second nœud existe avant le déploiement — faisait
échouer l'étape « Apply application Ingress ». Elle sort maintenant proprement
avec un message qui dit quoi faire.
proxy-buffers-number et proxy-busy-buffers-size ne sont plus lus depuis que le
buffering est désactivé : nginx passe la réponse au client au fil de l'eau et
ne garde qu'un seul buffer, dimensionné par proxy_buffer_size. Les laisser
donnait à croire que la taille des buffers était réglée.

proxy-buffer-size reste, avec la raison : c'est lui qui borne la lecture des
en-têtes, et les cookies de session Supabase dépassent le défaut de 4k.

Réf: nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering
- whitelist Cloudflare en deux étapes, avec le contrôle préalable qui évite de
  couper le site (le SNAT d'Octavia peut masquer l'IP source). Les plages sont
  lues depuis la source officielle au moment de la pose plutôt que recopiées
  dans le dépôt, où elles se périmeraient en silence
- note sur la survie de l'annotation aux déploiements (Server-Side Apply ne
  touche pas aux champs d'un autre field manager)
- section surveillance : pourquoi un Prometheus in-cluster est disproportionné
  à ce dimensionnement, et les commandes à passer après un incident
- secret.example.yaml renvoyait à MIGRATION.md, qui n'existe pas
- `app/[lang]/layout.tsx` fait office de layout racine : son `notFound()` n'avait
  aucune frontière parente et remontait jusqu'à `global-error`, soit un 500 pour
  chaque chemin qu'un crawler tente (`/config.json`, un fichier statique absent)
- `app/not-found.tsx` lui donne une destination ; il porte son propre `<html>`
  pour la même raison que `global-error`
- la CI se déclenche désormais aussi sur `dev`, pour qu'une régression ne puisse
  plus atteindre `main` sans avoir été testée
Le commit récupéré du worktree posait ses couleurs et son texte en dur
(`#0a0a0a`, « Cette page n'existe pas »), alors que `global-error.tsx` — soumis
exactement aux mêmes contraintes, puisqu'il porte lui aussi son propre `<html>`
hors de tout layout — montrait déjà comment faire : `import './globals.css'`,
classes de tokens, `next/font`, et clés i18n. Les clés `notFoundTitle` et
`notFoundDescription` existaient déjà.

La détection de langue côté navigateur, dupliquée entre les deux écrans, part
dans `lib/i18n/client-language.ts`. Elle reste côté client à dessein : lire les
cookies au serveur ferait basculer en rendu dynamique une page que le build
prérend en statique.
Squasher `dev → main` écrit sur main un commit qui n'existe pas dans dev : git
ne peut plus savoir que les deux historiques se sont rencontrés, chaque PR
suivante reliste tout ce qui a déjà été livré, et l'écart se creuse à chaque
release. C'est l'origine des « plus de commits que prévu » observés sur les PR.

Le squash reste le bon outil pour une branche de feature, éphémère par nature.
…d'auth

next 16.2.10 → 16.2.12, qui ferme huit CVE. Une compte plus que les autres
ici : le contournement de middleware/proxy en App Router. `proxy.ts` porte le
guard d'authentification du projet, donc un contournement du proxy est un
contournement de l'auth sur les routes protégées.

Les sept autres : SSRF dans les Server Actions, SSRF via les rewrites, déni de
service en App Router, déni de service sur l'API d'optimisation d'images,
confusion de cache sur les corps de réponse (×2), charge utile non bornée en
runtime Edge, et divulgation non authentifiée des IDs de Server Actions.

sharp est forcé en >=0.35.0 : Next réclame ^0.34.5, dont libvips traîne
CVE-2026-33196. Le forçage est sans risque — `images.unoptimized` fait que
sharp n'est jamais appelé, et ignoredBuiltDependencies l'empêche d'être
compilé.

brace-expansion n'a pas eu besoin d'override : le bump d'eslint-config-next
tire minimatch@10, qui prend déjà une version saine.

`pnpm audit` : 0 vulnérabilité.
Validé : ESLint, tsc, 326 tests unitaires, build production, 49 tests E2E.
Mesuré sur iPhone 13 : la card fait 191 px de haut, le bloc de contenu en
faisait 260 avec le titre du jour et 296 avec un titre sur trois lignes. Le
débordement était systématique, pas un cas limite — `overflow-hidden` le
masquait en rognant le badge et le haut du titre.

`text-4xl` déborde dès que le titre passe à deux lignes, ce qui arrive à partir
de 23 caractères selon les glyphes. Un palier indexé sur la longueur de la
chaîne serait donc faux : « WWW WWW WWW WWW WWW WWW » (23) tient sur deux
lignes là où « Spider-Man: Brand New Day » (25) tient sur une. `text-3xl` plus
un `line-clamp-2` couvre tous les cas mesurés — jusqu'à 75 caractères — avec
11 px de marge au pire, et reste plus lisible que le `text-2xl` qu'un palier
aurait imposé aux titres longs.

Le synopsis passe en `sm:` : sur 191 px il coûtait 52 px pour deux lignes
illisibles. Espacements resserrés sur mobile uniquement, desktop inchangé
(marge de 218 px, vérifiée dans Chrome).

Vérifié dans les deux thèmes, mobile et desktop. 326 tests unitaires,
49 tests E2E.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant