Audit Technique Complet - Plateforme MJY Holding
Date : 7 juillet 2026
Périmètre : src/, server.js, netlify/functions/, seed-db.js, configuration, documentation, historique Git
Version auditée : commit f00ce48 (branche main)
1. Résumé exécutif
Le projet a réussi sa migration d'un monolithe index.html (~4 200 lignes) vers une architecture React 18 + Vite modulaire, avec persistance Supabase (PostgreSQL + Realtime + RLS) et authentification par PIN signée en JWT côté serveur. La séparation en pages / components / services / utils est globalement saine et lisible.
Cependant l'audit relève :
| Gravité | Nombre | Exemples |
|---|---|---|
| 🔴 Critique | 4 | setState pendant le rendu (Error #310), incohérence de champ ts/date sur les notes, PIN en clair, secret JWT par défaut |
| 🟠 Majeur | 6 | Duplication server.js ↔ netlify/functions, dossiers hooks/ et context/ vides, listes d'admins incohérentes, workflow dev cassé |
| 🟡 Mineur | 8 | Textes obsolètes, cloudClient fantôme, absence d'ESLint, tests minimaux, requêtes N+1, etc. |
Note globale de structure : conforme aux conventions React à ~70 %. Les fondations sont bonnes, mais la logique métier (auth, CRUD, synchro) n'a jamais été extraite dans hooks/ / context/ comme prévu par audit_refactoring.md - elle reste dispersée dans les pages et dans un singleton module (storage._cache).
2. Conformité à la structure d'un projet React
2.1 Arborescence observée
src/
├── App.jsx # Racine + routage par rôle + cycle d'hydratation
├── main.jsx # Point d'entrée ReactDOM
├── index.css
├── assets/ # Logos + .gitkeep
├── components/
│ ├── common/ # Button, Modal, Tag, BackupButtons, SyncMenu
│ ├── tasks/ # TaskCard, TaskDetail, TaskForm, StatsBar,
│ │ # GroupedListView, PhaseGroupView, VocalCreator
│ ├── projects/ # ProjectModal, PhasesManager
│ ├── members/ # MemberModal
│ └── import/ # ImportModal
├── pages/ # Auth, AdminDashboard, SuperManagerDashboard, EmployeeDashboard
├── services/ # supabaseClient.js, storage.js
├── utils/ # taskHelpers.js
├── hooks/ # ⚠️ VIDE (.gitkeep seulement)
├── context/ # ⚠️ ABSENT (jamais créé)
└── __tests__/ # App.test.jsx
2.2 Verdict par convention
| Convention attendue | État | Commentaire |
|---|---|---|
pages/ (écrans) |
✅ Conforme | 4 écrans bien séparés. (« pages » ≈ « screens », convention valide.) |
components/ par domaine |
✅ Conforme | Découpage common / tasks / projects / members / import exemplaire. |
services/ |
✅ Conforme | Accès Supabase + couche de cache isolés. |
utils/ |
✅ Conforme | Helpers purs. |
hooks/ |
🟠 Non respecté | Dossier vide. useAuth, useTasks, useRealtime planifiés dans audit_refactoring.md jamais implémentés. La logique vit dans les pages. |
context/ |
🟠 Absent | Aucun React Context. L'état partagé transite par props et par un singleton module _cache + le hack global window.__forceAppRerender. |
2.3 Y a-t-il des éléments « au mauvais endroit » ?
Oui, quelques-uns :
Logique métier dans les pages plutôt que dans
hooks/.AdminDashboard.jsx(622 lignes) etSuperManagerDashboard.jsx(433 lignes) contiennent chacun la logique complète de CRUD tâches/projets/membres, la synchro et les filtres.saveMember,deleteProject,handleImportCreate,changeRole… devraient être des hooks réutilisables (useMembers,useProjects). Le code desaveMember/deleteMember/saveProject/deleteProjectest quasi dupliqué à l'identique entreAdminDashboardetSuperManagerDashboard.Re-export parasite dans un composant.
components/common/SyncMenu.jsx:151re-exporteforcePullFromCloud, forcePushToCloud, wipeCloud. Un composant UI ne doit pas ré-exporter des fonctions de service : c'est le rôle deservices/storage.js.window.__forceAppRerender(App.jsx:16) : mécanisme de rafraîchissement global viawindow. C'est un contournement de l'absence de state manager/Context - à remplacer par un Context ou un store.Constantes de sécurité dans
utils/.ADMIN_PIN,EMPLOYEE_PIN,SUPER_MANAGER_PIN,SUPER_ADMIN_EMAILSsont dansutils/taskHelpers.js- donc livrés dans le bundle client. Ces valeurs sensibles n'ont rien à faire côté front (voir §4).
3. Vérification demandée : imports en début de fichier
✅ Tous les import statiques sont bien placés en tête de fichier. Aucun import statique indenté / au milieu d'un fichier détecté (grep sur src/, netlify/, server.js, seed-db.js).
⚠️ Réserve - imports dynamiques redondants. App.jsx et Auth.jsx utilisent import('./services/...') à l'intérieur de fonctions, sur des modules déjà chargés statiquement ailleurs :
| Fichier | Ligne(s) | Module importé dynamiquement | Déjà importé statiquement ? |
|---|---|---|---|
App.jsx |
24, 35, 141 | ./services/supabaseClient (setSessionToken) |
Oui, indirectement via storage.js |
App.jsx |
52, 58, 112 | ./services/storage (subscribeToRealtimeChanges, pullFromSupabase) |
Oui - App.jsx:7 importe déjà storage |
Auth.jsx |
79 | ../services/supabaseClient (setSessionToken) |
- |
Ces import() n'apportent aucun code-splitting utile (le module est déjà dans le graphe). Ils compliquent la lecture et pourraient masquer des erreurs. Recommandation : remplacer par des imports statiques en tête de fichier (ex. App.jsx:7 → ajouter subscribeToRealtimeChanges, pullFromSupabase).
4. Problèmes 🔴 CRITIQUES
C1 - setState pendant le rendu (source de l'« Error #310 » récurrente)
Fichier : src/App.jsx
Plusieurs mutations d'état sont déclenchées pendant le rendu de App, ce qui est interdit par React et provoque exactement l'erreur que les 4 derniers commits tentent de corriger :
App.jsx:166→setEmployeeMode(false)appelé dans le corps de rendu (branche admin).App.jsx:181→if (!ws) { onLogout(); return null; }:onLogout()appellesetSession(null)pendant le rendu.
Impact : re-rendus en boucle possibles, avertissements React, comportement instable au changement de rôle. Ces corrections doivent passer par un useEffect (déplacer la logique de garde hors du rendu).
C2 - Champ de date des notes incohérent (ts vs date) → « Invalid Date »
Fichiers : src/components/tasks/TaskDetail.jsx, src/services/storage.js, docs/supabase_schema.sql
TaskDetail.jsx:78crée une note avec le champts:{ id, text, author, role, ts }.TaskDetail.jsx:249affichen.ts.- Mais
storage.js(lecture:173, écriture:246) et la tabletask_notes(schéma:77) utilisent la colonnedate.
Conséquence : une note s'affiche correctement juste après sa création (objet en mémoire avec ts), puis après un aller-retour cloud / rafraîchissement Realtime, n.ts devient undefined → « Invalid Date ». Le champ ts n'est jamais persisté.
Correctif : uniformiser sur date (créer la note avec date:, afficher n.date).
C3 - Codes PIN et e-mails admin en clair côté client
Fichier : src/utils/taskHelpers.js (lignes 1–4)
export const ADMIN_PIN = "1052";
export const EMPLOYEE_PIN = "4011";
export const SUPER_MANAGER_PIN = "9999";
export const SUPER_ADMIN_EMAILS = [
"j.gros1052@gmail.com",
"sanceymarco@hotmail.com",
];
Ces valeurs sont compilées dans le bundle JS public et même affichées à l'écran dans Auth.jsx:156 et :161 (« Code manager partagé (1052) », « (4011) »). De plus, la colonne members.pin est stockée en clair dans PostgreSQL (supabase_schema.sql:46), alors que audit_refactoring.md prévoyait un pin_hash.
Impact : n'importe quel visiteur peut se connecter en tant que manager/collaborateur. La sécurité repose entièrement sur des secrets publics.
Correctif : ne jamais exposer de PIN côté client ; hacher les PIN en base (bcrypt) ; supprimer les PIN partagés par défaut.
C4 - Secret JWT par défaut 'secret'
Fichiers : server.js:17, netlify/functions/auth.js:6
const jwtSecret = process.env.SUPABASE_JWT_SECRET || "secret";
Si SUPABASE_JWT_SECRET n'est pas défini en production, tous les JWT sont signés avec 'secret' → n'importe qui peut forger un token super_manager et contourner le RLS Supabase.
Correctif : faire échouer le démarrage si le secret est absent (pas de fallback silencieux).
5. Problèmes 🟠 MAJEURS
M1 - Backend dupliqué : server.js (Express) ↔ netlify/functions/*
Les trois endpoints (/api/auth, /api/public_data, /api/import) sont implémentés deux fois : dans server.js (hébergement Node/Hostinger) et dans netlify/functions/{auth,public_data,import}.js (serverless). Toute correction doit être répliquée manuellement. La dérive a déjà commencé (voir M2).
Recommandation : extraire la logique commune (signJWT, requêtes membres) dans un module partagé importé par les deux cibles.
M2 - Listes d'administrateurs incohérentes entre client et serveur
- Client (
taskHelpers.js:4) : super-admins =j.gros1052@gmail.com,sanceymarco@hotmail.com. - Serveur (
server.js:105etnetlify/functions/auth.js:130) : admins globaux =marco@myj.com,julien@myj.com.
Ces domaines ne correspondent pas. Selon le point d'entrée, un utilisateur peut être admin côté UI mais pas côté token (ou l'inverse). Source de bugs de permissions.
M3 - Dossiers hooks/ et context/ vides malgré le plan d'architecture
audit_refactoring.md (§3) planifie useAuth, useTasks, useRealtime et une couche services étoffée. Rien n'a été extrait : hooks/ ne contient qu'un .gitkeep, context/ n'existe pas. La logique reste couplée aux composants (voir §2.3). Dette d'architecture : réutilisation et testabilité fortement limitées.
M4 - Workflow de développement npm run dev cassé pour l'API
vite.config.js ne définit aucun proxy pour /api/*. En dev pur (vite), les appels fetch("/api/public_data") (Auth.jsx:13) et fetch("/api/auth") (Auth.jsx:64) échouent (pas de backend). Le routage /api/* n'existe que via netlify.toml (prod Netlify) ou server.js (qui sert le dist déjà buildé).
Impact : impossible de tester l'auth en npm run dev sans lancer netlify dev ou builder + node server.js. À documenter et/ou ajouter un server.proxy dans vite.config.js.
M5 - Requêtes Supabase N+1 et pull global systématique
Fichier : src/services/storage.js
writeToSupabaseupsert une ligne à la fois dans des bouclesfor(tâches:225, notes:248, phases:274, membres:298). Pour un projet de 50 tâches → 50+ requêtes réseau par sauvegarde.subscribeToRealtimeChanges(:339) déclenche unpullFromSupabase()complet (toutes les tables, tous les projets) à chaque changement Realtime.
Recommandation : utiliser des upserts batch (upsert([...])) et un pull ciblé par table/projet.
M6 - Dérive documentaire (docs ≠ code réel)
audit_refactoring.md(§4) décrit un schéma avectitle,position,pin_hash, table de liaisonproject_members(M2M). Le schéma réel (supabase_schema.sql) utiliselabel,order,pin(clair), et unproject_iden FK directe (1-N).README.mdmentionne un projet « KOBIKE » inexistant dansINIT_WS, et une synchro « toutes les ~4 secondes » alors que le code est passé au Realtime WebSocket.SyncMenu.jsx(:44,:110) parle encore de la « Netlify Functionstorage» et du polling 4 s - vestiges de l'ancienne architecture Blobs.
6. Problèmes 🟡 MINEURS / dette technique
- MI1 -
cloudClientfantôme.storage.js:434:cloudClient.delete()est un no-op (return { success: true }) mais est appelé lors des suppressions de projet (AdminDashboard.jsx:209,SuperManagerDashboard.jsx:128) en laissant croire à un nettoyage cloud. La suppression réelle repose sur la logique de diff dewriteToSupabase. À supprimer ou implémenter. - MI2 - Aucun outil de qualité. Pas d'ESLint, Prettier ni
.editorconfig. Le.gitignoreréférence pourtant.eslintcache. Aucun garde-fou sur les hooks React (aurait détecté C1). - MI3 - Couverture de tests minimale. Un seul fichier (
App.test.jsx, 3 assertions de montage). LefetchdeAuth.jsxn'est pas mocké → la suite crache unTypeError: Invalid URL /api/public_databruyant (test passe quand même). Aucune couverture des services, du workflow de validation, ni des permissions. - MI4 - Styles inline massifs. 100 % du style est en
style={{…}}inline (aucun CSS module / classe). Rendu correct mais duplication énorme (dizaines de répétitions defontFamily:"inherit", palettes hex copiées-collées) et pas de thème centralisé. - MI5 - Binaire versionné.
BIREF PLATEFORME MJY.pdf(68 Ko) est suivi par Git. Les documents binaires alourdissent le dépôt ; à externaliser ou déplacer hors du versionnement source. - MI6 -
console.*résiduels (10 occurrences danssrc/) laissés en production (storage.js,App.jsx). - MI7 -
VITE_ANTHROPIC_API_KEY(.env.example) : le préfixeVITE_est exposé au bundle client par Vite. La clé n'est utilisée que côté serveur (server.js:215) - la renommerANTHROPIC_API_KEYpour éviter toute fuite accidentelle si elle était lue côté front. - MI8 -
pollCloudmort.storage.js:355: fonction vidée « conservée pour compatibilité », toujours importée dansApp.jsx:7. Import inutile.
7. Inventaire documenté du code (fichier par fichier, fonction par fonction)
Objectif : documenter le rôle de chaque module et de chaque fonction/action de l'application (demande explicite « documentes tout le code et chaque action et fonction »).
7.1 Racine & configuration
| Fichier | Rôle |
|---|---|
index.html |
Gabarit HTML Vite, monte #root, charge src/main.jsx. |
vite.config.js |
Config Vite + Vitest (jsdom, setupFiles). Manque un proxy /api. |
netlify.toml |
Publie dist, mappe /api/* → /.netlify/functions/:splat. |
playwright.config.js |
Config E2E Playwright (e2e/). |
package.json |
Scripts : dev, build, preview, test, test:watch, test:e2e, start. |
seed-db.js |
Script d'amorçage de la base Supabase (projets/membres/PIN de démo). |
7.2 server.js (backend Express - hébergement Node)
| Fonction | Rôle |
|---|---|
signJWT(payload, secret) |
Signe un JWT HS256 en pur Web Crypto (Node 18+). |
POST /api/auth |
Authentifie par e-mail + PIN (cas spécial super@manager/9999), lit members via clé Service Role, renvoie { token, session }. Calcule le rôle effectif. |
GET /api/public_data |
Retourne projets + membres (sans PIN) + phases, pour peupler l'écran de login sans authentification. |
getFallbackStructuredProject(...) |
Parseur heuristique de brief projet si aucune clé Anthropic. |
POST /api/import |
Appelle l'API Anthropic pour structurer un brief en JSON (projet/phases/membres/tâches), sinon fallback. |
GET * |
Fallback SPA → dist/index.html. |
7.3 netlify/functions/ (équivalents serverless - dupliquent 7.2)
| Fichier | Rôle |
|---|---|
auth.js |
handler = équivalent de POST /api/auth (gère CORS/OPTIONS). |
public_data.js |
Équivalent de GET /api/public_data. |
import.js |
Équivalent de POST /api/import. |
7.4 src/App.jsx - racine applicative
| Élément | Rôle |
|---|---|
État hydrated / tick / syncStatus |
Cycle d'hydratation initial + compteur de re-rendu forcé + statut de synchro. |
useEffect (hydratation) |
Charge le token de session, hydrateFromCloud(), positionne le token Supabase. |
useEffect (Realtime) |
Abonnement onCloudUpdate + subscribeToRealtimeChanges + refresh sur visibilitychange. |
useEffect (expiration 1 h) |
Déconnexion auto après 1 h (session_created_at). |
useEffect (pull post-login) |
pullFromSupabase() dès qu'une session existe. |
onLogin / onLogout |
Ouverture / fermeture de session (efface token + cache local). |
| Bloc de routage | Aiguillage par rôle : Auth / SuperManagerDashboard / AdminDashboard / EmployeeDashboard. ⚠️ contient les setState-en-rendu C1. |
7.5 src/services/
storage.js - fonction |
Rôle |
|---|---|
loadS(k, fb) / saveS(k, v) |
Lecture/écriture dans le cache mémoire _cache ; localStorage réservé à la session ; déclenche la synchro cloud différée. |
_flushSoon / _flush / flushNow |
Débounce (300 ms) et vidage des écritures en attente vers Supabase. |
withWriteLock(fn) |
Verrou anti-course pendant les écritures massives (imports). |
pullFromSupabase() |
Charge projets/phases/membres/tâches/notes et reconstruit _cache (mapping snake_case → camelCase). |
writeToSupabase(k, v) |
Route une clé ws_* vers la bonne table + upsert + suppression par diff. ⚠️ N+1 (M5). |
hydrateFromCloud() |
Hydratation initiale (token + pull). |
subscribeToRealtimeChanges() |
Abonnement Realtime Postgres → pull global (M5). |
onCloudUpdate / pollCloud |
Listeners de mise à jour ; pollCloud = mort (MI8). |
exportData / importData |
Sauvegarde/restauration JSON locale. |
forcePullFromCloud / forcePushToCloud / wipeCloud |
Actions manuelles de SyncMenu. |
cloudClient.list/delete |
Vestige ; delete = no-op (MI1). |
supabaseClient.js - setSessionToken(jwt) |
Recrée le client Supabase avec l'en-tête Authorization: Bearer (pour le RLS). |
7.6 src/utils/taskHelpers.js
Constantes de sécurité (⚠️ C3), isSuperAdmin, validateMemberPin, daysLeft, fmtDate, dueBadge, PRIORITY_CFG, getPriority, getStatus, getPhaseProgress, uid, seeds PILLARS_DEFAULT / INIT_WS / INIT_MEMBERS.
7.7 src/pages/
| Page | Actions principales |
|---|---|
Auth.jsx |
Sélection de compte + saisie PIN → POST /api/auth ; pré-charge public_data. |
AdminDashboard.jsx |
reloadAll, saveTask, updateTask, deleteTask, saveProject, deleteProject, handleImportCreate, handleImportAppend, saveMember, deleteMember, filtres par projet/axe/statut, bascule Liste/Phases. |
SuperManagerDashboard.jsx |
Vue globale (stats par manager), gestion collaborateurs (changeRole, getEffectiveRole), gestion projets ; saveProject/deleteProject/saveMember/deleteMember dupliqués depuis Admin. |
EmployeeDashboard.jsx |
Tâches assignées multi-projets, onglets todo/en cours/à valider/faites, création de tâche perso, workflow de soumission (pending_validation). |
7.8 src/components/
| Composant | Rôle |
|---|---|
common/Button (Btn) |
Bouton stylé (variants primary/danger/ghost/success). |
common/Modal |
Boîte de dialogue générique. |
common/Tag |
Badge coloré. |
common/BackupButtons |
Export/import JSON local. |
common/SyncMenu |
Panneau de synchro cloud manuel (⚠️ textes obsolètes MI, re-export §2.3). |
tasks/TaskCard |
Carte tâche + changement rapide de statut. |
tasks/TaskDetail |
Détail tâche : statut, priorité, assignation (super manager), workflow validation, journal de notes (⚠️ C2). |
tasks/TaskForm |
Formulaire création/édition (mode selfMode collaborateur). |
tasks/StatsBar |
Compteurs cliquables = filtre. |
tasks/GroupedListView / PhaseGroupView |
Regroupement par pilier/responsable ou par phase. |
tasks/VocalCreator |
Dictée vocale (SpeechRecognition) + parsing d'échéance. |
projects/ProjectModal / PhasesManager |
Édition projet ; gestion des phases (ordre, J1→Jn). |
members/MemberModal |
Édition collaborateur (email, rôle, PIN, projets). |
import/ImportModal |
Assistant d'import IA (analyse → complétion emails → prévisualisation). |
8. Plan d'action priorisé
Priorité 1 - Corriger sans attendre (critiques)
- C1 : sortir
setEmployeeMode(false)/onLogout()du rendu deApp.jsx(les placer dans unuseEffect). - C2 : uniformiser le champ de note sur
date(créer et afficherdate, supprimerts). - C3/C4 : retirer les PIN du client et de l'UI, hacher les PIN en base, imposer
SUPABASE_JWT_SECRET(échec si absent).
Priorité 2 - Consolider (majeurs)
- M2 : unifier une seule source de vérité pour les admins (idéalement en base, pas en dur).
- M1 : factoriser la logique backend commune entre
server.jsetnetlify/functions. - M4 : ajouter un proxy
/apidansvite.config.js(ou documenternetlify dev). - M3 : extraire
useAuth,useTasks,useProjects/useMemberset unAppContext; dédupliquer Admin ↔ SuperManager. - M5 : upserts batch + pull ciblé.
Priorité 3 - Hygiène (mineurs)
- Ajouter ESLint (
eslint-plugin-react-hooks- aurait détecté C1) + Prettier. - Mettre à jour
README.md,audit_refactoring.md,SyncMenu(retirer KOBIKE, polling 4 s, Netlify Blobs). - Nettoyer
cloudClient,pollCloud,console.*, renommerVITE_ANTHROPIC_API_KEY. - Étoffer les tests (services, workflow de validation, permissions) et mocker
fetchdans les tests.
Fin du rapport - audit réalisé le 7 juillet 2026.
