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
- Script d'audit statique (
grep/regex surfetch(,catch {},if (!res.ok),toast.*("...")) pour lister tout point ou une reponse HTTP ou une exception peut se perdre sans toucher l'UI. - Triage manuel de chaque candidat — c'est l'etape qui a change le
diagnostic. Sur les ~90
catch"silencieux" et ~17if (!res.ok) returnremontes 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. - Sur les vrais candidats (mutations : suppression, achat, formulaire, toggle), verification ligne par ligne du chemin complet (declencheur → fetch → succes → echec → UI), et correction.
- Deuxieme passe ciblee sur l'i18n : grep de tout fichier qui appelle
useTranslationsET contient un texte d'etat (setError(...),toast.*(...)) capitalise hors d'un appelt(). Cette passe existe parce quepnpm i18n:auditne peut pas voir ce cas — son compteur de « strings en dur » exclut tout fichier qui appellet()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: aucuntry/catch, aucune verificationres.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: lescatchne faisaient queconsole.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 :
| Fichier | Ce qui etait en dur | Langue |
|---|---|---|
patterns/domain/domain-verification.tsx | 4 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'onboarding | Anglais |
(minimal)/scan/tracking/_components/home-cta.tsx | mot de passe vide, erreur reseau | Francais, dans le funnel public multi-locale |
(minimal)/scan/tracking/_components/scan/scan-input.tsx | placeholder URL, 2 erreurs de validation, 2 aria-labels, libelle du bouton | Francais, meme funnel |
composer-plus-menu/connectors-inline.tsx | garde-fou "aucune boutique selectionnee" | Anglais |
integrations/notion/notion-add-instruction.tsx | succes + echec d'ajout d'instruction | Anglais |
store-browser/.../scaffolds/elements.tsx (7 toasts) | demarrage picker, chat indisponible, copie selecteur, ouverture Worktree — succes et echec | Anglais |
workflow/nodes/codebase-editor/codebase-editor.tsx | sauvegarde de fichier — en cours / succes / echec | Anglais |
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—DomainListporte le meme trou i18n queDomainVerificationCardavant 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 duStoreBrowsergarde 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— vertpnpm 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 orphelinenode scripts/derive-client-namespaces.mjs— synchronise a chaque etapepnpm test— suite complete verte (11 242 tests passes, 0 echec, 0 regression) apres chaque lot de changements et apres le merge demain