# Shell applicatif (navigation, layout, auth, design system) _30 constats vérifiés — 5 haute, 18 moyenne, 7 basse._ ## Pages analysées ### `* (shell desktop ≥768px : Layout + Sidebar, entoure toutes les routes desktop)` **Rôle :** Cadre commun desktop : menu latéral filtré par rôle, en-tête (météo, date, sélecteur de magasin), zone de contenu des pages, déconnexion, envoi BAP. **Tâches principales de l'utilisateur :** - Naviguer entre les modules (19 entrées pour un admin, 8 pour un employé) - Choisir le magasin actif (admin/directeur/manager) - Réduire/étendre le menu - Envoyer un BAP (admin) - Se déconnecter **Appels API :** | Endpoint | Déclencheur | Handler serveur | Notes | |---|---|---|---| | GET /api/user | au montage de RouterProduction, puis à nouveau au montage de Layout (useAuthUnified), de Sidebar (useAuthSimple) et de chaque page/modale qui appelle useAuthUnified | server/localAuth.ts:204 (handler) + passport.deserializeUser server/localAuth.ts:141 -> storage.getUserWithGroups (server/storage.ts:371) appelé 2 fois = 4 requêtes SQL par appel | fetch manuel cache:'no-cache' sans cache partagé en production : 5 appels au premier affichage du tableau de bord | | GET /api/groups | au montage de Layout, mais seulement après la résolution du /api/user propre à Layout (enabled: !!user) — cascade | server/routes.ts:970 -> storage.getUserWithGroups (redondant avec req.user) + storage.getGroups (admin) | alimente le sélecteur de magasin et StoreContext.stores | | GET /api/weather/current | au montage de Layout + polling toutes les 30 min (refetchInterval), retry:1 | server/routes.ts:5671 -> storage.getWeatherSettings, storage.getWeatherData x2, weatherService.fetchCurrentWeather (API externe) si absent du cache | 404 si météo non configurée -> réessayé une fois à chaque chargement | | POST /api/logout | au clic sur Déconnexion (Sidebar) | server/localAuth.ts:200 logoutHandler (req.logout + session.destroy + redirect /auth) | suivi de window.location.href='/auth' (rechargement complet) | | GET /api/webhook-bap-config puis POST vers webhook externe | au clic Envoyer BAP | server/routes.ts:186 (config) ; envoi direct navigateur -> https://workflow.ffnancy.fr/... (Sidebar.tsx:121,143) | bloqué par CSP connect-src 'self' quand servi par server/index.ts | **Lisibilité / simplicité :** Menu dense (19 entrées / 5 sections pour un admin) avec sections déséquilibrées (« ANALYSES » = 1 entrée), libellés ambigus (Commandes vs Commandes Client, Échéance, Rapprochement, BAP), icônes dupliquées (BarChart3 pour Tableau de bord et Statistiques). L'accueil « / » n'active aucune entrée. L'en-tête est occupé par la météo et la date (widgets plus hauts que l'en-tête) au lieu du titre de page ; un employé ne voit jamais son magasin. Chargement en 3 temps (spinner plein écran, puis spinner du menu, puis sélecteur qui apparaît). Deux bleus concurrents, contraste du bouton primaire insuffisant, survol des boutons primaires inopérant, coins carrés dans les composants de base mais arrondis dans les pages, 62 !important dans index.css. ### `* (shell mobile <768px : MobileApp + MobileLayout + MobileBottomNav)` **Rôle :** Cadre mobile : en-tête compact avec magasin, barre de navigation basse (4 onglets + Plus), panneau latéral profil/magasin/déconnexion. **Tâches principales de l'utilisateur :** - Accéder rapidement à Accueil, Commandes, Livraisons, Tâches - Ouvrir « Plus » pour Calendrier, Publicités, Cmd Client, DLC, SAV, Avoirs - Changer de magasin et se déconnecter via le menu hamburger **Appels API :** | Endpoint | Déclencheur | Handler serveur | Notes | |---|---|---|---| | GET /api/user | au montage de RouterProduction, MobileApp, MobileLayout, MobileBottomNav et de la page (5 appels) | server/localAuth.ts:204 + deserializeUser server/localAuth.ts:141 -> storage.getUserWithGroups x2 | MobileBottomNav récupère user sans jamais l'utiliser | | GET /api/groups | au montage de MobileApp après son propre /api/user | server/routes.ts:970 -> storage.getUserWithGroups + storage.getGroups | pas d'invalidation au changement de magasin (contrairement au desktop) | | POST /api/logout | au clic Déconnexion dans le Sheet | server/localAuth.ts:200 logoutHandler | suivi de window.location.href='/auth' | **Lisibilité / simplicité :** Navigation mobile incomplète : Contacts, Rapprochement, Échéance, Statistiques et toute l'administration sont inaccessibles ; si on y arrive par URL, la page desktop s'affiche sans en-tête ni barre de navigation (impasse). Le bouton hamburger n'ouvre pas de menu de navigation mais seulement profil/magasin/déconnexion. Les onglets Commandes/Livraisons/Avoirs sont montrés à tous les rôles alors que le desktop les cache aux employés. Libellés de 10 px, rôle affiché en anglais (« Employee »), libellés différents du desktop (Accueil/Tableau de bord, Cmd Client/Commandes Client). Une rotation du téléphone en paysage (≥768px) bascule toute l'application en interface desktop. ### `/auth (et premier chargement : index.html, bundle, bootstrap)` **Rôle :** Connexion et démarrage de l'application : téléchargement du bundle, vérification de session, redirection vers l'accueil. **Tâches principales de l'utilisateur :** - Se connecter - Arriver sur le tableau de bord (ou la page demandée) **Appels API :** | Endpoint | Déclencheur | Handler serveur | Notes | |---|---|---|---| | GET /api/user | au montage de RouterProduction puis de AuthPage (2 appels 401 pour un visiteur non connecté) | server/localAuth.ts:204 | spinner plein écran bloquant pendant le premier appel | | GET /api/default-credentials-check | au montage de AuthPage | server/localAuth.ts:234 -> storage.getUserByUsername('admin') | la carte « admin / admin » est affichée par défaut avant la réponse | | POST /api/login | au clic Se connecter | server/localAuth.ts:153 (passport local) | en production : setTimeout 500 ms puis window.location.href='/' (rechargement complet, URL d'origine perdue) | | GET /assets/*.js (5 fichiers, 1,72 Mo brut / ~436 Ko gzip) + https://replit.com/public/js/replit-dev-banner.js | chargement initial de la page | server/index.production.ts:143-144 express.static sans maxAge ni compression | toutes les pages et recharts sont téléchargées avant même l'écran de connexion | **Lisibilité / simplicité :** Premier affichage lent : 1,08 Mo de code applicatif + 410 Ko de recharts préchargés avant l'écran de connexion, script tiers replit.com synchrone qui retarde le démarrage, pas de cache long ni de compression côté Node. Après connexion : attente artificielle de 500 ms puis rechargement complet, la page demandée initialement est perdue. Les identifiants par défaut « admin / admin » clignotent à l'écran pour tout visiteur. Langue du document déclarée en anglais et zoom bloqué. ### `(toute URL inconnue) — page 404` **Rôle :** Page affichée quand l'URL ne correspond à aucune route. **Tâches principales de l'utilisateur :** - Comprendre que la page n'existe pas - Revenir à l'accueil **Lisibilité / simplicité :** Message en anglais destiné aux développeurs (« 404 Page Not Found / Did you forget to add the page to the router? »), aucun bouton de retour ; min-h-screen alors que la page est rendue dans la zone de contenu du Layout. ## Constats | ID | Sév. | Catégorie | Effort | Titre | Fichier | |---|---|---|---|---|---| | [SHELL-01](#shell-01) | haute | perf-api | M | Chaque composant refait son propre GET /api/user en production (aucun cache partagé) | `client/src/hooks/useAuthUnified.ts:148` | | [SHELL-02](#shell-02) | haute | bug | S | Le tableau de bord lance ses requêtes avant de connaître le rôle : doublons et données non filtrées en cache | `client/src/pages/Dashboard.tsx:79` | | [SHELL-03](#shell-03) | haute | perf-bundle | M | Toutes les pages importées statiquement : 1,72 Mo de JS avant l'écran de connexion | `client/src/components/RouterProduction.tsx:7` | | [SHELL-04](#shell-04) | haute | perf-client | S | Script tiers replit.com synchrone livré en production : retarde le démarrage de l'app | `client/index.html:19` | | [SHELL-10](#shell-10) | haute | ux-simplicite | M | Mobile : 8 modules inaccessibles et pages desktop affichées sans aucune navigation | `client/src/components/RouterProduction.tsx:117` | | [SHELL-05](#shell-05) | moyenne | perf-serveur | S | Fichiers statiques servis sans compression ni cache long (le middleware « compression » est factice) | `server/index.production.ts:143` | | [SHELL-06](#shell-06) | moyenne | perf-client | S | Valeur de StoreContext recréée à chaque rendu + re-rendu global à chaque pixel de redimensionnement | `client/src/components/Layout.tsx:86` | | [SHELL-07](#shell-07) | moyenne | perf-api | S | Changement de magasin : invalidation par prédicat qui double les requêtes | `client/src/components/Layout.tsx:138` | | [SHELL-08](#shell-08) | moyenne | perf-api | S | Retry systématique sur 403/404/500 et redirections 401 par rechargement complet | `client/src/lib/queryClient.ts:79` | | [SHELL-09](#shell-09) | moyenne | ux-simplicite | S | Connexion = attente artificielle de 500 ms + rechargement complet, et la page demandée est perdue | `client/src/pages/AuthPage.tsx:68` | | [SHELL-11](#shell-11) | moyenne | coherence-design | M | Trois sources de vérité différentes pour « qui voit quel module » | `client/src/components/Sidebar.tsx:247` | | [SHELL-12](#shell-12) | moyenne | lisibilite | S | Menu : sections déséquilibrées, libellés ambigus, icônes dupliquées, libellés différents du mobile | `client/src/components/Sidebar.tsx:225` | | [SHELL-14](#shell-14) | moyenne | ux-simplicite | M | En-tête occupé par la météo et la date (qui débordent) au lieu du titre de page et du magasin | `client/src/components/Layout.tsx:106` | | [SHELL-15](#shell-15) | moyenne | ux-simplicite | S | Directeur/manager multi-magasins : alerte rouge bloquante au lieu d'une présélection ; pastilles couleur invalides | `client/src/components/Layout.tsx:195` | | [SHELL-16](#shell-16) | moyenne | accessibilite | M | Deux bleus concurrents et bouton primaire au contraste insuffisant (3,1:1) | `client/src/index.css:16` | | [SHELL-17](#shell-17) | moyenne | bug | S | Les modificateurs d'opacité (hover:bg-primary/90...) ne génèrent aucun CSS : pas de retour visuel au survol | `tailwind.config.ts:44` | | [SHELL-18](#shell-18) | moyenne | coherence-design | M | Coins incohérents : composants de base carrés, pages et mobile arrondis | `client/src/components/ui/button.tsx:8` | | [SHELL-19](#shell-19) | moyenne | dette-code | M | index.css : 62 !important et règles globales qui écrasent bordures, focus et couleurs d'anneau | `client/src/index.css:280` | | [SHELL-20](#shell-20) | moyenne | accessibilite | S | Langue déclarée en anglais et zoom bloqué sur mobile | `client/index.html:2` | | [SHELL-21](#shell-21) | moyenne | accessibilite | S | Boutons icône sans nom accessible et éléments cliquables non focusables | `client/src/pages/mobile/MobileBottomNav.tsx:86` | | [SHELL-22](#shell-22) | moyenne | lisibilite | S | Tailles de texte trop petites dans la navigation (10-11 px) | `client/src/pages/mobile/MobileBottomNav.tsx:76` | | [SHELL-25](#shell-25) | moyenne | ux-simplicite | M | Rotation ou redimensionnement : toute l'application est démontée et remontée ; branches mobiles mortes dans Layout/Sidebar | `client/src/components/RouterProduction.tsx:106` | | [SHELL-28](#shell-28) | moyenne | bug | S | Les identifiants par défaut « admin / admin » s'affichent par défaut sur l'écran de connexion | `client/src/pages/AuthPage.tsx:25` | | [SHELL-13](#shell-13) | basse | ux-simplicite | S | Sur l'accueil « / » (page d'arrivée après connexion) aucune entrée du menu n'est active | `client/src/components/Sidebar.tsx:219` | | [SHELL-23](#shell-23) | basse | lisibilite | S | Page 404 en anglais avec message destiné aux développeurs | `client/src/pages/not-found.tsx:11` | | [SHELL-24](#shell-24) | basse | lisibilite | S | Rôle affiché en anglais sur mobile, libellé recalculé à la main sur desktop | `client/src/pages/mobile/MobileLayout.tsx:136` | | [SHELL-26](#shell-26) | basse | dette-code | S | Code mort et dupliqué dans le shell (hooks, nav, fichiers .tmp, scripts publics) | `client/src/components/Sidebar.tsx:394` | | [SHELL-27](#shell-27) | basse | bug | S | Appel de hook dans une fonction asynchrone (forceAuthRefresh) et comportement dev/prod divergent | `client/src/hooks/useAuthUnified.ts:129` | | [SHELL-29](#shell-29) | basse | dette-code | M | Fonction métier BAP (≈300 lignes) logée dans le menu, avec destinataires et URL codés en dur | `client/src/components/Sidebar.tsx:794` | | [SHELL-30](#shell-30) | basse | ux-simplicite | S | Un seul ErrorBoundary global : une erreur dans une page efface aussi le menu | `client/src/App.tsx:10` | ### SHELL-01 **Chaque composant refait son propre GET /api/user en production (aucun cache partagé)** — perf-api, sévérité haute, effort M - **Fichier :** `client/src/hooks/useAuthUnified.ts:148` - **Constat :** useAuthUnified.ts:8-11 `const isDevelopment = ... (hostname === 'localhost' || ...replit.dev) && import.meta.env.DEV === true;` -> en prod la branche React Query est désactivée (`enabled: isDevelopment`, l.43) et chaque instance exécute son propre `useEffect(() => { ... fetch('/api/user', { credentials: 'include', cache: 'no-cache' ...` (l.148-167) avec un état local `useState(true)` (l.24). 48 fichiers appellent useAuthUnified() (RouterProduction:56, Layout:22, Dashboard:16, AnnouncementCard:101, MobileApp:16, MobileLayout:22, MobileBottomNav:47, toutes les modales...), et Sidebar.tsx:44 utilise en plus useAuthSimple qui refait le même fetch (useAuthSimple.ts:15). Côté serveur, chaque appel coûte 4 requêtes SQL : deserializeUser server/localAuth.ts:141-143 `storage.getUserWithGroups(id)` puis le handler server/localAuth.ts:211 rappelle `storage.getUserWithGroups(userId)` (getUser + jointure, storage.ts:371-388). Conséquence visible : spinner plein écran (RouterProduction.tsx:79-88), puis spinner du menu (Sidebar.tsx:392-445), puis /api/groups qui attend le /api/user propre au Layout (Layout.tsx:48-51 `enabled: !!user`). - **Impact :** Premier affichage du tableau de bord desktop = 5 appels /api/user (RouterProduction, Layout, Sidebar, Dashboard, AnnouncementCard) = 20 requêtes SQL rien que pour l'authentification ; mobile = 5 appels ; +1 à chaque changement de page et à chaque ouverture de modale. Cascade de 3 états de chargement successifs et sauts de mise en page. - **Recommandation :** Hook unique basé sur React Query : `useQuery({ queryKey: ['/api/user'], queryFn: fetchUser /* dédié : 401 → return null, SANS redirection */, staleTime: Infinity, retry: false })`, réexporté par useAuthUnified, useAuthSimple, useAuthProduction, useAuth ; supprimer isDevelopment. Après login : `await queryClient.invalidateQueries({ queryKey: ['/api/user'] })` (ou faire renvoyer `userGroups` par /api/login avant d'utiliser setQueryData). Côté serveur : /api/user et /api/groups utilisent `req.user` (déjà chargé par deserializeUser avec userGroups) au lieu de rappeler getUserWithGroups. ### SHELL-02 **Le tableau de bord lance ses requêtes avant de connaître le rôle : doublons et données non filtrées en cache** — bug, sévérité haute, effort S - **Fichier :** `client/src/pages/Dashboard.tsx:79` - **Constat :** Dashboard.tsx:16 `const { user } = useAuthUnified();` vaut null au montage en production (état local du hook). Pourtant les requêtes partent tout de suite sans `enabled` : l.79 `const ordersUrl = `/api/orders${selectedStoreId && user?.role === 'admin' ? `?storeId=${selectedStoreId}` : ''}`` (idem l.80-81) -> la clé change quand user arrive => 2 requêtes, la première sans storeId (tous magasins = la plus lourde). Pour /api/stats/monthly (l.29 clé `['/api/stats/monthly', selectedStoreId]`, filtre l.37 `if (selectedStoreId && user?.role === 'admin')`), /api/stats/yearly (l.62) et /api/tasks (l.285) la clé ne contient pas le rôle : la réponse non filtrée est mise en cache sous la clé du magasin sélectionné. - **Impact :** Pour un admin avec un magasin sélectionné : statistiques et tâches de TOUS les magasins affichées comme si c'étaient celles du magasin (jusqu'au prochain refetch), et 3 grosses requêtes doublées à chaque arrivée sur le tableau de bord. - **Recommandation :** Ajouter `enabled: !!user` aux useQuery de Dashboard.tsx (stats/monthly l.29, stats/yearly l.55, orders/deliveries/customer-orders l.84-94, announcements/recent, tasks l.282). L'ajout de `user?.role` dans les queryKey est optionnel. Correction définitive via SHELL-01. ### SHELL-03 **Toutes les pages importées statiquement : 1,72 Mo de JS avant l'écran de connexion** — perf-bundle, sévérité haute, effort M — vérification : partiellement confirmé - **Fichier :** `client/src/components/RouterProduction.tsx:7` - **Constat :** RouterProduction.tsx:5-44 : 24 pages desktop + 11 pages mobiles en `import X from "@/pages/..."` ; aucun `lazy(`/``. recharts n'est importé que par pages/Analytics.tsx:14 ; jsbarcode que par pages/CustomerOrders.tsx:33. De plus NocoDBConfig, DatabaseDebug, BackupManager, WeatherSettings sont importés (l.17-18, 22, 27) mais jamais utilisés comme composant de route. (framer-motion, react-icons et xlsx ne sont importés nulle part côté client : pas d'impact bundle.) - **Impact :** Un employé sur le PC du magasin ou en 4G télécharge et parse ~436 Ko gzip (1,72 Mo brut) — dont les graphiques et le code admin qu'il n'utilisera jamais — avant de voir l'écran de connexion. - **Recommandation :** 1) Corriger manualChunks pour que vendor-charts ne contienne que recharts/d3/victory (forme fonction : `id.includes('node_modules/recharts') || id.includes('victory-vendor') || id.includes('d3-')`), ou retirer l'entrée vendor-charts, et vérifier dans dist/public/index.html que vendor-charts n'est plus en modulepreload. 2) Puis `lazy(() => import(...))` pour toutes les routes sauf AuthPage/Dashboard (et équivalents mobiles), `}>` dans la zone de contenu du Layout/MobileLayout. 3) Supprimer les 4 imports inutilisés (sans gain de taille tant qu'Utilities les importe). ### SHELL-04 **Script tiers replit.com synchrone livré en production : retarde le démarrage de l'app** — perf-client, sévérité haute, effort S - **Fichier :** `client/index.html:19` - **Constat :** client/index.html:19 `` est recopié tel quel dans dist/public/index.html. Le bundle est un `