Conversation
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.
…lisible sur tout contenu
…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.
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.