Audits · septembre 2026Audit des etats produit : 11 septembre 2026

Audit des etats produit : 11 septembre 2026

Perimetre demande : initial, loading/skeleton, empty, partial, stale data, offline/network failure, permissions, validation, timeout, retry, success, actions destructives — sur l'ensemble du produit (marketing, minimal…

Perimetre demande : initial, loading/skeleton, empty, partial, stale data, offline/network failure, permissions, validation, timeout, retry, success, actions destructives — sur l'ensemble du produit (marketing, minimal, dashboard, chat/agents). Livre par PR #1050. Ce document et les items de backlog qu'il ouvre sont la sortie complete de l'audit.

Ce qui a ete trouve, en une phrase

Le defaut dominant n'etait pas l'absence d'etats de chargement (le produit en a deja beaucoup, bien faits) mais un faux succes : une mutation qui echoue cote serveur et que l'UI resout exactement comme si elle avait reussi — fermeture de dialogue, redirection, champ vide — parce que personne n'avait verifie res.ok. Le second defaut, plus diffus, est du texte d'etat (succes/echec/en cours) laisse dans la langue source alors que le composant qui le porte est deja branche sur next-intl pour tout le reste.

Methode

  1. Script d'audit statique (grep/regex sur fetch(, catch {}, if (!res.ok), toast.*("...")) pour lister tout point ou une reponse HTTP ou une exception peut se perdre sans toucher l'UI.
  2. Triage manuel de chaque candidat — c'est l'etape qui a change le diagnostic. Sur les ~90 catch "silencieux" et ~17 if (!res.ok) return remontes par le script, la tres grande majorite sont des lectures en arriere-plan deliberement fail-soft (sondage de statut au montage, polling, cache local) deja commentees comme telles dans le code (/* fail-soft — keep the default fallback */, /* best-effort — fetch failure keeps the tile in its default state */). Les traiter comme des bugs aurait degrade l'UX : un badge de connecteur qui affiche une erreur bloquante parce que le sondage silencieux du statut a rate une fois serait pire que l'etat neutre actuel. Cette distinction — mutation declenchee par l'utilisateur (doit toujours parler) vs lecture de fond (peut rester silencieuse si le fallback reste correct) — est le filtre applique a chaque candidat.
  3. Sur les vrais candidats (mutations : suppression, achat, formulaire, toggle), verification ligne par ligne du chemin complet (declencheur → fetch → succes → echec → UI), et correction.
  4. Deuxieme passe ciblee sur l'i18n : grep de tout fichier qui appelle useTranslations ET contient un texte d'etat (setError(...), toast.*(...)) capitalise hors d'un appel t(). Cette passe existe parce que pnpm i18n:audit ne peut pas voir ce cas — son compteur de « strings en dur » exclut tout fichier qui appelle t() au moins une fois (limite deja documentee, platform-ops/0330, growth-web-content-i18n-23) — donc un composant traduit a 95 % qui garde 5 % de texte source est structurellement invisible au garde automatique.

Corrige

Faux succes sur des actions destructives (le trou le plus severe)

SettingsDropdown (menu contextuel header/sidebar, compte / org / boutique) et une seconde boite de suppression de boutique dans la sidebar ne verifiaient jamais le statut de la reponse DELETE. Un refus serveur (abonnement actif, pas proprietaire, 500) resolvait exactement comme un succes : dialogue ferme, callback de succes declenche, redirection — l'utilisateur croyait l'entite supprimee alors qu'elle existait toujours. Meme defaut sur les trois telechargements « exporter mes donnees » (rien ne se passait, aucun message) et sur le bouton « annuler » de l'onglet Activity (resultat jete sans etre lu). Point notable : ~/settings de l'organisation gerait deja cette suppression correctement — seul le chemin rapide du menu contextuel etait casse. Deux chemins vers la meme action, un seul juste.

Actions avec echec non signale

  • CreditsDropdown (redeem / achat de credits) : un code invalide, un montant refuse ou une coupure reseau ne montraient rien, le panneau restait fige.
  • FeedbackDropdown + user-actions.handleFeedbackSubmit : aucun try/catch, aucune verification res.ok — une soumission rejetee ou hors-ligne videait le champ comme si elle avait reussi.
  • AutonomySelector : un PATCH en echec ramenait la selection a l'ancien niveau sans jamais dire pourquoi.
  • OAuthGrantsBlock.revoke : couper l'acces d'un client IA (action sensible cote securite) n'avait aucun retour succes/echec.
  • ConfirmDeleteDialog : les catch ne faisaient que console.error, jamais rien pour l'utilisateur.
  • Page memoire d'organisation : cinq alert() natifs remplaces par des toasts, coherents avec le reste du produit.
  • openBillingPortal (~/billing) : un echec (Stripe indisponible, session expiree) ne faisait que logger en console — le bouton s'arretait de tourner sans qu'aucun message n'explique quoi que ce soit. Meme forme que les cas ci-dessus, trouvee en fin d'audit.

Absence de detection hors-ligne

Le produit n'avait aucune detection reseau (grep -rn "navigator.onLine" src/ ne retournait rien). useNetworkStatus (nouveau hook, src/hooks/use-network-status.ts) distingue « le navigateur n'a pas d'interface reseau » de « une requete vient d'echouer au niveau transport » (navigator.onLine peut mentir derriere un portail captif ou un VPN casse), et OfflineBanner (src/components/shared/connectivity/) l'affiche globalement.

Skeletons ad-hoc non harmonises

ui/skeleton (nouvelle primitive, reproduite a l'identique du registre shadcn new-york-v4 — le registre ui.shadcn.com repond 403 depuis une session distante, justification ecrite dans le fichier) remplace les blocs animate-pulse recopies a la main dans six loading.tsx a fort trafic ([orgSlug]/intelligence, ~/disputes, account/disputes, account/settings/payouts, (marketing)/intelligence, (marketing)/marketplace).

Texte d'etat laisse dans la langue source malgre next-intl deja branche

Meme defaut, six occurrences trouvees et corrigees, chacune avec sa justification propre :

FichierCe qui etait en durLangue
patterns/domain/domain-verification.tsx4 messages de resultat + le tri visuel base sur message.includes("successfully") (aurait rendu TOUJOURS "echec" dans les 5 autres locales)Anglais
patterns/forms/entity-form-fields.tsx (TagsField)2 erreurs de validation — champ utilise depuis les parametres compte, boutique et l'onboardingAnglais
(minimal)/scan/tracking/_components/home-cta.tsxmot de passe vide, erreur reseauFrancais, dans le funnel public multi-locale
(minimal)/scan/tracking/_components/scan/scan-input.tsxplaceholder URL, 2 erreurs de validation, 2 aria-labels, libelle du boutonFrancais, meme funnel
composer-plus-menu/connectors-inline.tsxgarde-fou "aucune boutique selectionnee"Anglais
integrations/notion/notion-add-instruction.tsxsucces + echec d'ajout d'instructionAnglais
store-browser/.../scaffolds/elements.tsx (7 toasts)demarrage picker, chat indisponible, copie selecteur, ouverture Worktree — succes et echecAnglais
workflow/nodes/codebase-editor/codebase-editor.tsxsauvegarde de fichier — en cours / succes / echecAnglais

Les deux fichiers du funnel scan public sont le cas le plus visible : c'est une surface publique, non authentifiee, deja traduite dans les six locales sur tout le reste de sa copie (badges, hints, plan d'action…) — un visiteur non francophone qui tapait un mot de passe vide ou une URL invalide lisait une phrase en francais au milieu d'une page dans sa langue.

Nouvelles cles ajoutees dans les six locales (en, fr, de, es, it, pt), toutes verifiees par pnpm i18n:audit (parite stricte, aucune fusion de fallback) : dashboard.org.billing.errors.portalFailed, patterns.domainVerification.{verify,verifying,successMessage, failureMessage,errorMessage}, patterns.entityForm.{invalidValue, alreadyAdded}, minimal.scan.homeCta.{emptyPassword,connectionError}, minimal.scan.ui.{urlRequired,urlInvalid,urlPlaceholder,scanButton, startScanAria,scanInProgressAria}, composer.menu.selectStoreFirst, chat.notion.{instructionAdded,addInstructionFailed}, features.workflow.{pickerStartFailed,chatNotAvailable,elementAttached, selectorCopied,copyFailed,openingInWorktree,worktreeNotReady}, features.workflow.editor.{savingFile,savedFile,saveFileFailed}.

Volontairement inchange

  • Le repertoire genere par CLI patterns/ai-elements/ (regle du depot : ne jamais editer a la main).
  • Les indicateurs de statut « point qui pulse » (semantique differente d'un skeleton de chargement).
  • Le motif de shimmer du bouton d'auth dans app-header.tsx / multi-org-store-picker.tsx / ai-chat.tsx : signale en commentaire comme valide, garde coherent entre les trois plutot que remplace isolement.
  • Les ~80 lectures de fond fail-soft identifiees a l'etape 2 de la methode (sondage de statut de connecteur, polling de branches, cache local) : correctes telles quelles, deja commentees comme deliberees.

Ce qui reste (backlog)

Deux items ouverts pour le reliquat concret trouve pendant cette passe, tous deux petits (taille S) et non bloquants :

  • design-system/0607 — DomainList porte le meme trou i18n que DomainVerificationCard avant correction, mais zero importeur dans toute la codebase : a supprimer ou brancher + traduire, pas a corriger a l'aveugle.
  • ai-platform/0608 — le panneau DevTools Elements du StoreBrowser garde trois libelles statiques (pas des messages d'etat transitoires, donc hors du scope strict de cet audit) en anglais dur a cote de texte deja traduit.

Le probleme structurel qui rend ces deux cas invisibles a pnpm i18n:audit — le compteur de strings en dur exclut tout fichier appelant t() au moins une fois — est deja track et priorise P3 dans platform-ops/0330 (growth-web-content-i18n-23) ; cet audit n'ouvre pas de doublon, il fournit sept instances concretes en plus (six corrigees, une listee en 0608) qui confirment que la limite documentee la produit du vrai angle mort, pas seulement une imprecision theorique du script.

Verification

  • pnpm typecheck — vert
  • pnpm lint (fichiers touches, puis passe complete) — vert, 0 warning (--max-warnings 0)
  • pnpm i18n:audit — parite stricte des six catalogues a chaque etape, 0 cle manquante, 0 orpheline
  • node scripts/derive-client-namespaces.mjs — synchronise a chaque etape
  • pnpm test — suite complete verte (11 242 tests passes, 0 echec, 0 regression) apres chaque lot de changements et apres le merge de main