mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-05-18 08:18:15 +02:00
Triage du 95_a_capitaliser.md (~75 propositions) : - 60 entrées intégrées dans knowledge/ (backend, frontend, workflow) - 4 nouveaux fichiers : backend/patterns/tests.md, backend/risques/tests.md, frontend/patterns/general.md, workflow/patterns/general.md - 6 doublons rejetés - Mise à jour des READMEs index pour refléter les nouvelles entrées - 95_a_capitaliser.md restauré à sa structure initiale - 40_decisions_et_archi.md : décision mono-tenant déployable vs SaaS multi-tenant - 90_debug_et_postmortem.md : sub-agents Write indisponible, effet iceberg CI, prisma migrate diffs cosmétiques Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
793 lines
32 KiB
Markdown
793 lines
32 KiB
Markdown
# Frontend — Risques & vigilance : Général
|
|
|
|
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/risques/README.md` pour l'index complet.
|
|
|
|
---
|
|
|
|
<a id="risque-accessibilite-oubliee"></a>
|
|
## Accessibilité oubliée (a11y)
|
|
|
|
### Risques
|
|
|
|
- App inutilisable au clavier/lecteur d'écran
|
|
- Régressions silencieuses sur focus/labels
|
|
|
|
### Symptômes
|
|
|
|
- Modales impossibles à fermer au clavier
|
|
- Inputs sans labels/erreurs non annoncées
|
|
- Focus "perdu"
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Checklist a11y minimale sur chaque écran clé
|
|
- Gestion de focus (modales, erreurs formulaire)
|
|
- Labels/aria cohérents + tests simples
|
|
|
|
---
|
|
|
|
<a id="risque-regex-globale-singleton-lastindex"></a>
|
|
## Regex globale `/g` en singleton — bug `lastIndex` stateful
|
|
|
|
### Risques
|
|
|
|
- Une regex avec flag `/g` ou `/y` définie comme constante au niveau module maintient un état `lastIndex` entre les appels
|
|
- `String.prototype.replace()` réinitialise `lastIndex`, mais `.test()` ou `.exec()` ne le font pas → bug stateful difficile à détecter, souvent introduit par un refactor ultérieur
|
|
|
|
### Symptômes
|
|
|
|
- `.test(str)` retourne alternativement `true` / `false` sur la même chaîne selon l'ordre d'appel
|
|
- Bug non reproductible en isolation, uniquement en séquence d'appels
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```typescript
|
|
// ❌ RISQUÉ — regex globale partagée entre tous les appels
|
|
const LINK_PATTERN = /https?:\/\/\S+/gi;
|
|
function processLinks(content: string) {
|
|
return content.replace(LINK_PATTERN, ...); // OK today
|
|
// Mais si quelqu'un ajoute LINK_PATTERN.test(x) ailleurs → bug lastIndex
|
|
}
|
|
|
|
// ✅ SÛR — nouvelle instance à chaque appel, aucun état partagé
|
|
function makeLinkPattern(): RegExp {
|
|
return /https?:\/\/\S+/gi;
|
|
}
|
|
function processLinks(content: string) {
|
|
return content.replace(makeLinkPattern(), ...);
|
|
}
|
|
```
|
|
|
|
- **Règle** : les regex avec flag `/g` ou `/y` utilisées pour transformation de strings → toujours créer via une factory, jamais en singleton de module
|
|
|
|
- Contexte technique : TypeScript / React Native — app-alexandrie 24-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-alert-prompt-ios-only"></a>
|
|
## `Alert.prompt` iOS-only — fonctionnalité silencieusement cassée sur Android
|
|
|
|
### Risques
|
|
|
|
- `Alert.prompt` ne déclenche rien sur Android (retourne `undefined` silencieusement).
|
|
- Les tests unitaires passent (mock), mais le flux ne s'exécute jamais sur 50 % des devices en production.
|
|
|
|
### Symptômes
|
|
|
|
- Flux de saisie utilisateur qui fonctionne sur simulateur iOS mais est inactif sur Android
|
|
- Aucun message d'erreur côté dev ni côté utilisateur
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
1. Ne jamais utiliser `Alert.prompt` dans un projet Expo cross-platform.
|
|
2. Remplacer par une modale custom : `Modal` + `TextInput` React Native — portable, accessible, testable.
|
|
3. Wrapper le `TextInput` dans `KeyboardAvoidingView` avec `behavior={Platform.OS === 'ios' ? 'padding' : 'height'}`.
|
|
|
|
- Contexte technique : React Native / Expo cross-platform — app-alexandrie 31-03-2026
|
|
|
|
---
|
|
|
|
<a id="risque-primitive-ui-couplee-contexte-parent"></a>
|
|
## Primitive UI couplée au contexte parent (layout ou namespace métier)
|
|
|
|
### Risques
|
|
|
|
- Une primitive générique (`PageShell`, `ContentCard`, `SectionWrapper`) qui embarque des classes de surface, de largeur ou de namespace métier devient non réutilisable hors de son premier contexte
|
|
- Le couplage reste silencieux au lint et au typecheck, puis force l'ajout progressif de props `variant`, `layout`, `width` ou de classes externes contradictoires
|
|
|
|
### Symptômes
|
|
|
|
- La primitive applique directement des classes comme `.card`, `.card--dashboard`, `.dashboard__item`, `.profile__card`
|
|
- Le parent doit contourner le style natif de la primitive pour l'utiliser dans un autre écran
|
|
- Les classes `namespace__element` fuitent dans des composants supposés agnostiques du domaine
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Une primitive pose le squelette sémantique ; le parent pose la surface visuelle (card, width, background, espacement de contexte)
|
|
- Ne pas injecter de classes de namespace métier sur une primitive générique via `class`
|
|
- Si une variation réutilisable existe vraiment, l'exprimer via une API explicite et bornée (`tone`, `variant`) plutôt que par des classes métier ad hoc
|
|
- Contexte technique : Vue 3 / CSS modulaire — RL799_V2, 02-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-migration-partielle-composant-classes-legacy"></a>
|
|
## Migration partielle vers un composant standard — classes legacy conservées
|
|
|
|
### Risques
|
|
|
|
- La coexistence de classes legacy (`.primary`, `.ghost`, `.danger`) et de classes du nouveau composant (`.app-btn--primary`, `.app-btn--ghost`) crée une ambiguïté durable de convention
|
|
- Les nouveaux développements continuent d'utiliser l'ancien système faute de règle claire, ce qui ralentit la standardisation
|
|
|
|
### Symptômes
|
|
|
|
- Deux façons de produire la même affordance coexistent dans le même repo
|
|
- Un composant dédié existe, mais des liens ou boutons continuent d'utiliser les anciennes classes globales
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Lorsqu'un composant standardise une affordance, supprimer en même temps les classes CSS globales équivalentes
|
|
- Si un reliquat legacy doit rester temporairement, documenter explicitement son périmètre et sa date de sortie attendue
|
|
- En review, traiter toute nouvelle utilisation d'une classe legacy équivalente comme une régression de standardisation
|
|
- Contexte technique : Vue 3 / design system léger — RL799_V2, 02-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-aria-roles-sans-clavier"></a>
|
|
## ARIA roles sans comportement clavier associé
|
|
|
|
### Risques
|
|
|
|
- Poser `role="menu"` / `role="menuitem"` sur un composant sans implémenter le pattern clavier donne une fausse impression d'accessibilité
|
|
- Les rôles ARIA trompent les lecteurs d'écran et violent WCAG 2.1 (4.1.2 Name, Role, Value)
|
|
|
|
### Symptômes
|
|
|
|
- `role="menu"` sans fermeture via `Escape`
|
|
- Pas de navigation `ArrowUp` / `ArrowDown` ni de roving tabindex
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Poser `role="menu"` / `role="menuitem"` implique obligatoirement :
|
|
- Fermeture via `Escape`
|
|
- Navigation via `ArrowUp` / `ArrowDown`
|
|
- Roving tabindex (`tabindex="0"` sur l'item actif, `-1` sur les autres)
|
|
- Focus automatique du premier item à l'ouverture
|
|
|
|
**Règle** : ne jamais poser un `role` ARIA de widget interactif sans implémenter le pattern clavier correspondant (cf. WAI-ARIA Authoring Practices)
|
|
|
|
- Contexte technique : Vue 3 / accessibilité — RL799_V2 03-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-duplication-logique-metier-monorepo"></a>
|
|
## Duplication de logique métier dans les composants UI (monorepo)
|
|
|
|
### Risques
|
|
|
|
- Dans un monorepo avec un package partagé (`shared`), les fonctions utilitaires métier (ex: conversion grade → rang) sont redéfinies localement dans les composants ou pages frontend
|
|
- Ce type de duplication silencieuse provoque des divergences à terme
|
|
|
|
### Symptômes
|
|
|
|
- Fonction `switch/case` ou mapping identique à une fonction déjà exportée par `shared`
|
|
- Même signature et même logique dans plusieurs fichiers de couches différentes (composant, page, service)
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Les fonctions utilitaires métier ne doivent jamais être redéfinies localement dans les composants ou pages frontend
|
|
- Importer systématiquement depuis le package partagé (`@monrepo/shared` ou équivalent) plutôt que de copier-coller la logique
|
|
- **Signal review** : grep des fonctions utilitaires existantes dans shared avant de valider un nouveau switch/case
|
|
|
|
- Contexte technique : Vue 3 / monorepo — RL799_V2 06-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-event-listeners-globaux-modales"></a>
|
|
## Event listeners globaux pour interactions modales
|
|
|
|
### Risques
|
|
|
|
- `window.addEventListener('keydown')` pour capturer Escape dans une modale crée un listener global qui peut confliter avec d'autres modales
|
|
- Le listener fuit si le composant est mal démonté
|
|
|
|
### Symptômes
|
|
|
|
- `window.addEventListener('keydown', handler)` dans un composant modale
|
|
- Cleanup dans `onBeforeUnmount` mais risque de fuite si le démontage échoue
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Utiliser `@keydown.escape` directement sur l'élément dialog avec `tabindex="-1"` + focus automatique à l'ouverture
|
|
- Élimine le besoin de cleanup et scope l'interaction au composant
|
|
- **Signal review** : dans tout composant modale, vérifier que les listeners clavier sont sur l'élément, pas sur `window`
|
|
|
|
- Contexte technique : Vue 3 / modales — RL799_V2 06-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-boutons-imbriques"></a>
|
|
## Boutons imbriqués dans les listes interactives
|
|
|
|
### Risques
|
|
|
|
- Un `<button>` ou `<a>` contenant un autre élément interactif (bouton, lien) est du HTML invalide
|
|
- Casse l'accessibilité et produit un comportement imprévisible selon les navigateurs
|
|
|
|
### Symptômes
|
|
|
|
- `<button>` conteneur avec un `<button>` enfant (ex: étoile favori dans une carte cliquable)
|
|
- Comportement de clic imprévisible, événements qui ne remontent pas correctement
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Utiliser un `<div>` conteneur avec des boutons séparés côte à côte
|
|
- Si toute la ligne doit être cliquable, séparer la zone de clic principale (bouton content) de l'action secondaire (bouton étoile/action)
|
|
- **Signal review** : dans tout composant liste avec actions inline, vérifier qu'aucun élément interactif n'est imbriqué dans un autre
|
|
|
|
- Contexte technique : HTML / accessibilité — RL799_V2 06-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-fire-and-forget-sans-feedback"></a>
|
|
## Fire-and-forget sans feedback sur actions non-critiques
|
|
|
|
### Risques
|
|
|
|
- Une action asynchrone non-critique (cache IndexedDB, analytics, sync) lancée en fire-and-forget sans feedback masque les échecs
|
|
- L'utilisateur croit que l'action est faite (ex: document disponible hors-ligne) alors qu'elle a échoué
|
|
|
|
### Symptômes
|
|
|
|
- `.then(...).catch(() => {})` sur une action secondaire
|
|
- `catch { /* ignore */ }` sans log ni feedback visuel
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Même si l'action est non-bloquante, afficher un feedback discret en cas d'échec (toast, badge absent)
|
|
- L'utilisateur doit pouvoir distinguer "fait" de "échoué silencieusement"
|
|
- **Signal review** : tout `.catch(() => {})` ou `catch { /* ignore */ }` mérite au minimum un log ou un feedback visuel
|
|
|
|
- Contexte technique : frontend / actions async — RL799_V2 07-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-monorepo-shim-js-desynchronise"></a>
|
|
## Monorepo ESM — shim runtime `.js` désynchronisé de l'index TypeScript
|
|
|
|
### Risques
|
|
- Le typecheck passe mais le runtime navigateur casse (`named export not found`).
|
|
|
|
### Symptômes
|
|
- Erreur Vite/browser sur export absent alors que `index.ts` est correct.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Si un shim `.js` est maintenu, imposer une mise à jour miroir à chaque nouvel export.
|
|
- Ajouter un test/guard de cohérence exports TS vs JS shim.
|
|
|
|
- Contexte technique : monorepo / ESM shim runtime — RL799_V2 15-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-eslint-flat-tsconfigrootdir-manquant"></a>
|
|
## ESLint flat config TypeScript sans `tsconfigRootDir`
|
|
|
|
### Risques
|
|
- Erreurs de parsing massives en IDE/monorepo selon CWD d'exécution.
|
|
|
|
### Symptômes
|
|
- `No TsConfigRootDir` / `Cannot read tsconfig.json` alors que le build TS passe.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Toujours définir `tsconfigRootDir: import.meta.dirname` quand `parserOptions.project` est utilisé.
|
|
- Redémarrer le serveur ESLint après correction.
|
|
|
|
- Contexte technique : tooling / ESLint flat config — RL799_V2 17-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-pwa-auth-cookie-cache"></a>
|
|
## PWA + auth cookie httpOnly — stratégie de cache non maîtrisée
|
|
|
|
### Risques
|
|
- Réponses sensibles servies depuis cache offline.
|
|
- Comportement d'auth incohérent entre réseau/cached.
|
|
|
|
### Symptômes
|
|
- Session/app state divergents après activation SW ou reprise réseau.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Exclure explicitement les routes authentifiées sensibles du cache persistant.
|
|
- Définir une stratégie stricte par classe de route (auth, API privée, assets publics).
|
|
|
|
- Contexte technique : PWA / service worker / auth cookie — RL799_V2 18-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-pwa-beforeinstallprompt-tardif"></a>
|
|
## PWA install prompt — capture tardive de `beforeinstallprompt`
|
|
|
|
### Risques
|
|
- Événement perdu au cold boot, prompt jamais proposé.
|
|
|
|
### Symptômes
|
|
- Implémentation correcte en apparence mais aucun déclenchement sur Android.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Installer l'écouteur le plus tôt possible dans le cycle d'initialisation.
|
|
- Ne pas baser la détection iOS uniquement sur l'UA (cas iPad en mode desktop).
|
|
|
|
- Contexte technique : PWA / install prompt — RL799_V2 18-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-cache-pwa-soft-delete-fuite"></a>
|
|
## Cache offline PWA + soft-delete — invalidation diff-based scopée
|
|
|
|
### Risques
|
|
|
|
- Une PWA qui cache des blobs en IndexedDB pour offline reading **ne reçoit aucun signal automatique** quand un document est soft-deleted côté serveur
|
|
- Le contenu reste lisible hors-ligne indéfiniment. Pour des données réglementaires ou sensibles, c'est un gap de sécurité non négligeable
|
|
|
|
### Symptômes
|
|
|
|
- Document soft-deleted en base, encore consultable offline par les utilisateurs qui l'ont mis en cache
|
|
- Aucun mécanisme automatique de purge
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
**Mitigation V1 (best-effort, diff-based)** au prochain `loadEntries` online :
|
|
|
|
1. Lire la liste serveur courante (filtrée `deletedAt IS NULL`)
|
|
2. Lire les IDs cachés localement **scopés au même périmètre** (type+grade) — sinon on supprime à tort un doc d'un autre onglet
|
|
3. Diff : `cached - server = soft-deleted` → `removeCachedDocument(id)` pour chaque
|
|
|
|
```typescript
|
|
const bustCachedIfMissing = async (candidateIds: string[], serverIds: Set<string>) => {
|
|
for (const id of candidateIds) {
|
|
if (!serverIds.has(id)) await removeCachedDocument(id);
|
|
}
|
|
};
|
|
```
|
|
|
|
**Mitigation V2 (push server-initiated)** : Service Worker abonné à un canal (postMessage / WebSocket / SSE), serveur publie `{ type: 'document-soft-deleted', id }` sur soft-delete, SW intercepte et fait `caches.delete()` immédiat. Coût : infra push + gestion connectivité partielle. À garder en backlog si le risque devient critique (audit GDPR).
|
|
|
|
**Tests recommandés** :
|
|
- doc caché + recharge avec serveur qui omet ce doc → assert `removeCachedDocument` appelé
|
|
- doc caché + serveur qui retourne le doc → assert pas d'effet (non-régression)
|
|
- doc caché pour scope `rituels` + recharge sur scope `mementos` qui omet ce doc → assert pas d'effet (scope isolation)
|
|
|
|
- Contexte technique : PWA / IndexedDB — RL799_V2 20-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-duplication-focus-parent-modal"></a>
|
|
## Duplication parent ↔ modal de la capture de focus
|
|
|
|
### Risques
|
|
|
|
- Quand un composant modal implémente correctement le pattern a11y `previousActiveElement` (capture à `onMounted`, restitution à `close`/`submit`), le composant parent **ne doit PAS** stocker un `lastTrigger` en parallèle
|
|
- Code mort avec commentaire trompeur : un lecteur qui cherche à comprendre le flux focus va s'y perdre
|
|
|
|
### Symptômes
|
|
|
|
- Le parent a une ref `lastFabTrigger` / `lastTrigger` écrite au clic du trigger
|
|
- Elle n'est **jamais lue** — le commentaire prétend "pour restitution du focus par la modal", mais la modal a son propre mécanisme
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- **Une seule responsabilité** pour la restitution du focus : la modal
|
|
- Le parent se contente d'ouvrir la modal (`uploadModalOpen.value = true`)
|
|
- Le parent ne capture le focus QUE si la modal ne le fait pas (cas d'un overlay maison sans pattern `previousActiveElement`)
|
|
- **Repérage en code review** : `grep -n "lastTrigger\|previousActive" components/ pages/` → s'il y a des occurrences dans BOTH un parent et une modal du même flux, c'est le signal
|
|
|
|
- Contexte technique : Vue 3 / a11y modales — RL799_V2 20-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-touch-action-none-card-mobile"></a>
|
|
## `touch-action: none` sur card mobile bloque scroll vertical
|
|
|
|
### Risques
|
|
|
|
- Une card mobile gérant un swipe horizontal avec `touch-action: none` capture **tout** le toucher, laissant le JS gérer le scroll vertical
|
|
- Le JS détecte scroll vs swipe via un seuil mais doit **libérer** l'événement après l'avoir analysé → un délai imperceptible s'installe, le scroll vertical devient saccadé et souvent ignoré
|
|
- L'utilisateur trouve la liste "inscrollable" quand son pouce touche directement les cards
|
|
|
|
### Symptômes
|
|
|
|
- Poser le pouce sur une card puis scroller ne marche qu'une fois sur 20
|
|
- Scroll OK dans les marges vides à côté
|
|
- Aucune erreur console
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```css
|
|
/* AVANT — bug : scroll vertical capturé */
|
|
.member-card {
|
|
touch-action: none;
|
|
}
|
|
|
|
/* APRÈS — scroll natif OK, swipe horizontal toujours fonctionnel */
|
|
.member-card {
|
|
touch-action: pan-y;
|
|
}
|
|
```
|
|
|
|
Combiné avec un handler JS qui `preventDefault` uniquement sur mouvement horizontal significatif (> 10 px) :
|
|
|
|
```typescript
|
|
if (Math.abs(deltaX) > 10 && Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
event.preventDefault();
|
|
}
|
|
```
|
|
|
|
**Règle** :
|
|
- Card qui gère swipe horizontal ET scroll vertical : `touch-action: pan-y` (le navigateur gère nativement le scroll vertical, seul l'axe horizontal est laissé au JS)
|
|
- Card qui ne gère QUE du pan/zoom custom (rare) : `touch-action: none` peut se justifier
|
|
- Tous les autres cas : laisser la valeur par défaut (`auto`)
|
|
|
|
**Repérage en code review** : `grep -rn "touch-action: none" components/` → chaque occurrence est suspecte.
|
|
|
|
- Contexte technique : CSS / mobile — RL799_V2 21-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-button-wrapper-card-color-inherit"></a>
|
|
## `<button>` wrapper card — toujours `color: inherit` (reset user-agent)
|
|
|
|
### Risques
|
|
|
|
- Quand on utilise un `<button>` comme wrapper cliquable d'une card (pattern idiomatique pour l'a11y clavier), il hérite du `color` user-agent par défaut
|
|
- Sur certains setups (Safari/dark mode notamment), ce `color` peut être bleu — le texte enfant hérite et contamine titres et paragraphes qui devraient prendre la couleur du thème
|
|
|
|
### Symptômes
|
|
|
|
- Titre de card en bleu sur fond sombre alors que le thème prévoit de l'or
|
|
- Bug non visible en dev light mode sur Chrome — apparaît uniquement sur certains setups → difficile à reproduire
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```css
|
|
.my-card-button {
|
|
/* reset user-agent */
|
|
color: inherit;
|
|
font-family: inherit;
|
|
border: 0;
|
|
background: transparent;
|
|
/* … */
|
|
}
|
|
```
|
|
|
|
**Règle** : tout `<button>` qui wrappe du contenu stylé par le thème (card, liste, ligne de tableau) doit reset `color`, `font-family`, `font-size`, `border`, `background`. C'est un bootstrap user-agent minimal à prévoir dans tout design system.
|
|
|
|
**Alternative** : `<div role="button" tabindex="0">` avec `@keydown.enter/space`. Plus verbeux, mais évite les resets. Pattern valide si l'équipe est à l'aise avec les implications a11y.
|
|
|
|
- Contexte technique : CSS / a11y — RL799_V2 21-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-erreur-silencieuse-4-etats"></a>
|
|
## Erreur silencieuse = blanc indistinguable de "aucune donnée" — 4 états distincts (loading / empty / error / forbidden)
|
|
|
|
### Risques
|
|
|
|
- Un composant qui affiche le résultat d'un fetch sans distinguer ses 4 états produit du **blanc** dans 3 cas sur 4 — l'utilisateur ne peut pas savoir si la donnée est en chargement, légitimement vide, en erreur réseau, ou refusée par RBAC
|
|
- Sur les flows critiques (rituel, opérations sensibles), un blanc silencieux est inacceptable : l'utilisateur prend des décisions sur la base de l'affichage
|
|
|
|
### Symptômes
|
|
|
|
- Plusieurs cards d'une vue qui auto-fetchent et tombent toutes en `[]` côté front quand l'API renvoie 403 — affichage indistinguable de "vide légitime"
|
|
- Toast générique "Une erreur est survenue" sans corrélation avec un retry actionnable
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Tout composant qui fetch une ressource doit avoir **4 états distincts** dans son rendu :
|
|
|
|
1. **Loading** : skeleton, spinner, ou texte explicite (« Chargement… »)
|
|
2. **Empty (donnée légitimement vide)** : message explicite *« Aucune donnée enregistrée pour … »*
|
|
3. **Error (réseau / serveur)** : message + bouton retry. Ne jamais se contenter d'un blanc
|
|
4. **Forbidden (403)** : message explicite *« Vous n'avez pas accès à cette donnée »* + suggestion d'action (recharger / contacter admin)
|
|
|
|
Le frontend doit savoir **distinguer 403** des autres erreurs au niveau de son service HTTP, et propager l'info au composant. Ne pas traiter `!response.ok` en bloc avec un message générique.
|
|
|
|
```typescript
|
|
export const getXxx = async (id: string) => {
|
|
const response = await apiFetch(`/api/xxx/${id}`);
|
|
if (response.status === 403) throw new ForbiddenError(...);
|
|
if (response.status === 404) throw new NotFoundError(...);
|
|
if (!response.ok) throw new Error(await parseError(response));
|
|
return (await response.json()).data;
|
|
};
|
|
```
|
|
|
|
Le composant catche les types d'erreur et choisit le rendu approprié.
|
|
|
|
- Contexte technique : Vue 3 / fetch — RL799_V2 27-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-service-worker-non-secure-context"></a>
|
|
## Service Worker invisible en accès non-secure (HTTP via IP réseau)
|
|
|
|
### Risques
|
|
|
|
- Les Service Workers exigent un [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) — HTTPS strict, OU URL `http://localhost:*` ou `http://127.0.0.1:*`
|
|
- Un accès via IP réseau en HTTP (Tailscale `100.x.x.x`, LAN `192.168.x.x`) est non-secure → `navigator.serviceWorker` est `undefined` → la PWA fonctionne en mode "navigateur classique" mais sans cache offline, sans push, sans badge, sans installation
|
|
- Les tests E2E en Tailscale loupent silencieusement les régressions SW
|
|
|
|
### Symptômes
|
|
|
|
- `navigator.serviceWorker.register('/sw.js')` lève `Cannot read properties of undefined (reading 'register')`
|
|
- DevTools > Application > Service Workers ne montre rien
|
|
- Tests "ça marche en LAN" qui ne reflètent pas la prod HTTPS
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```javascript
|
|
// Détection
|
|
console.log(window.isSecureContext, location.protocol, location.hostname);
|
|
// true "https:" "..." → OK
|
|
// true "http:" "localhost" → OK
|
|
// false "http:" "192.168.1.42" → SW désactivé
|
|
```
|
|
|
|
Stratégies par contexte :
|
|
|
|
1. **Test local** : utiliser `http://localhost:<port>` strict (jamais l'IP, même en LAN)
|
|
2. **Test réseau / mobile** : reverse proxy HTTPS (Caddy/Traefik avec Let's Encrypt, ou `tailscale cert` pour le magicDNS Tailscale)
|
|
3. **Préview Vite** : `vite preview --https` avec un certificat auto-signé (acceptable en dev test)
|
|
|
|
**Préventif** :
|
|
- documenter dans le README projet que le SW exige HTTPS/localhost
|
|
- en CI E2E, toujours utiliser `localhost` (le webServer Playwright tourne sur `localhost` par défaut)
|
|
- ne PAS supposer qu'un test "ça marche en LAN" reflète la prod HTTPS
|
|
|
|
- Contexte technique : PWA / Service Worker — RL799_V2 28-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-vite-pwa-bascule-strategies-runtime-caching"></a>
|
|
## Vite-plugin-pwa : bascule `generateSW` → `injectManifest` rend `runtimeCaching` inerte
|
|
|
|
### Risques
|
|
|
|
- En mode `injectManifest`, Vite PWA n'injecte PAS de runtime workbox — il bundle le `src/sw.ts` fourni tel quel
|
|
- Toute la config `workbox.*` du `vite.config.ts` (sauf `globPatterns`/`globIgnores` déplacés sous `injectManifest.*`) est ignorée silencieusement, sans warning
|
|
- Régression directe : leak de cookies API en cache, contenu sensible en cache, 404 transformés en `index.html`
|
|
|
|
### Symptômes
|
|
|
|
- Après bascule, les routes runtime configurées dans `workbox.runtimeCaching` n'ont plus aucun effet
|
|
- Le build passe, aucune erreur visible, mais en DevTools > Application > Service Worker on ne voit pas les routes attendues
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Détection : examiner `dist/sw.js` après build — chercher des strings clés (`/api/`, `uploads`, `manifest.webmanifest`, `addEventListener`). Si elles sont absentes du SW custom, c'est qu'on a perdu la protection.
|
|
|
|
**Mitigation** : RÉIMPLÉMENTER À LA MAIN dans `src/sw.ts` toutes les routes runtime, denylist, et cleanup :
|
|
|
|
```typescript
|
|
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
|
|
import { registerRoute, NavigationRoute } from 'workbox-routing';
|
|
import { NetworkOnly, NetworkFirst } from 'workbox-strategies';
|
|
import { ExpirationPlugin } from 'workbox-expiration';
|
|
|
|
cleanupOutdatedCaches();
|
|
precacheAndRoute(self.__WB_MANIFEST);
|
|
|
|
registerRoute(({ url }) => url.pathname.startsWith('/api/'), new NetworkOnly());
|
|
registerRoute(({ url }) => url.pathname.startsWith('/uploads/'), new NetworkOnly());
|
|
registerRoute(
|
|
({ url }) => url.pathname === '/manifest.webmanifest',
|
|
new NetworkFirst({
|
|
cacheName: 'manifest-cache',
|
|
plugins: [new ExpirationPlugin({ maxAgeSeconds: 300, maxEntries: 1 })],
|
|
}),
|
|
);
|
|
|
|
registerRoute(
|
|
new NavigationRoute(async () => { /* fallback handler */ }, {
|
|
denylist: [/^\/api\//, /^\/uploads\//, /^\/manifest\.webmanifest$/],
|
|
}),
|
|
);
|
|
```
|
|
|
|
**Vérifications obligatoires post-bascule** (DevTools sur build/preview) :
|
|
|
|
1. Application > Service Worker : `sw.js` activé
|
|
2. Network : `POST /api/auth/login` → pas de "(from ServiceWorker)", pas de cache
|
|
3. Network : GET `/uploads/foo.pdf` → réseau direct
|
|
4. Network mode Offline : navigation `/page` → fallback `index.html` ; `/api/foo` → erreur réseau (PAS index.html)
|
|
5. Déploiement v2 : ancien cache purgé après activation
|
|
|
|
`setCatchHandler` ne suffit PAS à remplacer `navigateFallback` — il ne se déclenche que si une route enregistrée throw. Pour le navigation fallback, utiliser `NavigationRoute` explicitement.
|
|
|
|
- Contexte technique : Vite / vite-plugin-pwa / Workbox — RL799_V2 28-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-ts-strict-uint8array-buffersource"></a>
|
|
## TS strict — `Uint8Array<ArrayBufferLike>` non assignable à `BufferSource`
|
|
|
|
### Risques
|
|
|
|
- TS 5.7+ avec lib DOM récente paramètre `Uint8Array` par défaut sur `ArrayBufferLike` (qui inclut `SharedArrayBuffer`)
|
|
- Beaucoup d'APIs DOM (Push API, WebCrypto certaines surfaces) attendent un `BufferSource` strict avec `buffer: ArrayBuffer` — d'où une erreur TS au build
|
|
|
|
### Symptômes
|
|
|
|
```
|
|
Type 'Uint8Array<ArrayBufferLike>' is not assignable to type 'BufferSource'.
|
|
Types of property 'buffer' are incompatible.
|
|
Type 'ArrayBufferLike' is not assignable to type 'ArrayBuffer'.
|
|
Type 'SharedArrayBuffer' is missing the following properties from type 'ArrayBuffer'…
|
|
```
|
|
|
|
L'erreur est au build, pas au runtime (le code marche en JS).
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Créer explicitement un `ArrayBuffer` strict, puis remplir via une vue `Uint8Array` :
|
|
|
|
```typescript
|
|
// ❌ Ne compile pas en TS strict
|
|
const urlBase64ToUint8Array = (s: string): Uint8Array => {
|
|
const raw = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
|
|
const out = new Uint8Array(raw.length);
|
|
for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
|
|
return out;
|
|
};
|
|
|
|
// ✅ Compile et fonctionne identiquement
|
|
const urlBase64ToArrayBuffer = (s: string): ArrayBuffer => {
|
|
const raw = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
|
|
const buf = new ArrayBuffer(raw.length);
|
|
const view = new Uint8Array(buf);
|
|
for (let i = 0; i < raw.length; i++) view[i] = raw.charCodeAt(i);
|
|
return buf;
|
|
};
|
|
```
|
|
|
|
**Alternative déconseillée** : `as ArrayBuffer` cast — masque le problème, peut rater une vraie incompatibilité si la lib DOM évolue.
|
|
|
|
- Contexte technique : TypeScript 5.x / lib DOM — RL799_V2 28-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-safe-areas-ios-viewport-fit-cover"></a>
|
|
## Safe-areas iOS — `viewport-fit=cover` indispensable
|
|
|
|
### Risques
|
|
|
|
- Sur iPhone Pro/Max (notch + home indicator + Dynamic Island), `padding-top: env(safe-area-inset-top)` ou `padding-bottom: env(safe-area-inset-bottom)` retourne **0** sans `<meta viewport content="… viewport-fit=cover">`
|
|
- Le développeur conclut à tort que le pattern ne fonctionne pas, alors qu'il manque juste l'opt-in
|
|
|
|
### Symptômes
|
|
|
|
- Header fixed rogné par le notch
|
|
- Nav bottom rognée par le home indicator
|
|
- `env(safe-area-inset-*)` retourne 0 sur iPhone Pro+
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
**Pattern complet (3 endroits) à appliquer ensemble** :
|
|
|
|
```html
|
|
<!-- 1) index.html — opt-in safe-areas -->
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
```
|
|
|
|
```css
|
|
/* 2) Header sticky : top + safe-area-inset-top */
|
|
.app-header {
|
|
position: fixed;
|
|
top: 0;
|
|
padding-top: env(safe-area-inset-top);
|
|
height: calc(var(--size-header-height) + env(safe-area-inset-top));
|
|
}
|
|
|
|
/* 3) Nav bottom : bottom + safe-area-inset-bottom + safe-area-inset-left/right */
|
|
.app-nav {
|
|
position: fixed;
|
|
bottom: 0;
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
padding-left: env(safe-area-inset-left);
|
|
padding-right: env(safe-area-inset-right);
|
|
height: calc(var(--size-nav-height) + env(safe-area-inset-bottom));
|
|
}
|
|
|
|
/* FAB / menu flottant : bottom au-dessus de la nav + safe-area + offset */
|
|
.fab {
|
|
bottom: calc(var(--size-nav-height) + env(safe-area-inset-bottom) + 16px);
|
|
/* `max()` empêche les boutons d'être collés au bord rond du device */
|
|
right: max(16px, env(safe-area-inset-right));
|
|
}
|
|
```
|
|
|
|
**Règle de débogage** : si `env(safe-area-inset-bottom)` semble retourner 0 sur iPhone Pro+, **vérifier `<meta viewport>` AVANT de chercher ailleurs**. C'est presque toujours la cause.
|
|
|
|
`safe-area-inset-left` et `safe-area-inset-right` ne sont non-nuls qu'en mode paysage (notch latéral). Garder le `padding-left/right` quand même → no-op en portrait, fix en paysage.
|
|
|
|
- Contexte technique : CSS / iOS — RL799_V2 02-05-2026
|
|
|
|
---
|
|
|
|
<a id="risque-input-date-safari-ios-min-width"></a>
|
|
## `<input type="date">` Safari iOS — `appearance: none` + `min-width: 0` + `min-height` obligatoires
|
|
|
|
### Risques
|
|
|
|
- Sur Safari iOS (et Chrome iOS car webkit sous-jacent), un `<input type="date">` ou `datetime-local` :
|
|
- déborde de son conteneur sur la droite (largeur intrinsèque > 100 %)
|
|
- apparaît plus mince que les autres inputs (hauteur intrinsèque différente)
|
|
- affiche un styling natif iOS qui casse le design system
|
|
|
|
### Symptômes
|
|
|
|
- `width: 100%` ne suffit pas — la largeur intrinsèque écrase la contrainte
|
|
- Bug non reproductible sur Chrome desktop, visible uniquement sur iPhone réel ou Safari Responsive Design Mode
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```css
|
|
input[type="date"],
|
|
input[type="datetime-local"],
|
|
input[type="time"] {
|
|
appearance: none; /* neutralise le styling natif */
|
|
-webkit-appearance: none; /* Safari iOS — ne PAS oublier */
|
|
min-width: 0; /* permet à width: 100% de gagner */
|
|
min-height: 48px; /* aligne avec les autres inputs */
|
|
}
|
|
```
|
|
|
|
**Les 4 propriétés sont nécessaires** :
|
|
|
|
- `appearance: none` seul : le styling natif disparaît mais la largeur intrinsèque reste → débordement
|
|
- `min-width: 0` seul : le styling natif reste, on a juste cassé sa hauteur
|
|
- `min-height: 48px` : nécessaire pour homogénéiser avec les inputs text classiques
|
|
- `-webkit-appearance: none` : redondant en théorie avec `appearance: none` mais nécessaire en pratique sur certaines versions Safari iOS
|
|
|
|
- Contexte technique : CSS / Safari iOS — RL799_V2 01-05-2026
|
|
|
|
---
|
|
|
|
<a id="risque-fieldset-legend-flex-grid"></a>
|
|
## `<fieldset>` / `<legend>` cassent un layout flex inline
|
|
|
|
### Risques
|
|
|
|
- Pour un champ "label + valeur inline" (ex : `Grade [GradeBadge]` sur la même ligne), le réflexe sémantique est `<fieldset>` + `<legend>`
|
|
- `<legend>` a un comportement natif particulier : interrompt visuellement le `border` du fieldset, son `display` est traité spécialement, il ne se comporte pas comme un enfant flex/grid normal
|
|
- Le legend prend toute la largeur, l'input passe en dessous, impossible de les aligner sans hacks `position: absolute`
|
|
|
|
### Symptômes
|
|
|
|
- `<fieldset style="display: flex">` avec `<legend>` + `<input>` qui ne s'alignent pas sur la même ligne
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Pour un groupe de champs **avec layout custom** (flex/grid inline), utiliser `<div>` + `<span class="label">` + le champ :
|
|
|
|
```html
|
|
<!-- ❌ Layout cassé : legend ne se comporte pas comme un enfant flex -->
|
|
<fieldset class="field-inline">
|
|
<legend>Grade</legend>
|
|
<GradeBadge :grade="grade" />
|
|
</fieldset>
|
|
|
|
<!-- ✅ Layout custom OK : div + span — perte sémantique mineure
|
|
compensée par aria-labelledby si besoin -->
|
|
<div class="field-inline">
|
|
<span class="field-inline__label" id="grade-label">Grade</span>
|
|
<GradeBadge :grade="grade" aria-labelledby="grade-label" />
|
|
</div>
|
|
```
|
|
|
|
Garder `<fieldset>/<legend>` uniquement quand on accepte le rendu natif (groupe vertical avec border + legend qui chevauche le border supérieur — pattern formulaire admin classique).
|
|
|
|
**Trade-off à assumer** : `<fieldset>` apporte une sémantique a11y (groupe de champs liés). En remplaçant par `<div>`, on peut compenser via `role="group" aria-labelledby="..."` si le besoin d'a11y est fort. Pour un simple label+badge inline, c'est rarement nécessaire.
|
|
|
|
- Contexte technique : HTML / a11y / CSS — RL799_V2 02-05-2026
|
|
|
|
---
|