EPIDOCS — Erreurs courantes¶
Frontend¶
Page blanche après déploiement¶
Symptôme : L'application affiche une page blanche ou une erreur 500.
Cause : Les variables NEXT_PUBLIC_* ne sont pas injectées au build time.
Solution :
# Vérifier que les variables sont dans le .env sur le VPS
ssh root@VPS_IP
cat /root/projects/epidocs/epidocs-frontend/.env
# Les variables NEXT_PUBLIC_* doivent être présentes AVANT le build Docker
# Si elles manquent, les ajouter et reconstruire l'image
Warning
Les variables NEXT_PUBLIC_* sont injectées au build time, pas au runtime. Un simple restart du conteneur ne suffit pas — il faut rebuild l'image Docker.
Boucle de redirection /auth¶
Symptôme : L'utilisateur est redirigé en boucle vers /auth après connexion.
Cause possible 1 : Le cookie auth_token est bloqué par le navigateur (Domain invalide).
Solution :
# Vérifier dans les DevTools > Application > Cookies
# Si le cookie n'apparaît pas, c'est un problème de Domain
# Le backend doit configurer AUTH_COOKIE_DOMAIN = None en dev
# ou AUTH_COOKIE_DOMAIN = ".epitools.bj" en prod
Cause possible 2 : Le proxy Next.js ne lit pas correctement les cookies.
Solution : Vérifier que src/proxy.js utilise bien request.cookies.get() et non cookies() de next/headers.
Erreur 503 sur toutes les requêtes API¶
Symptôme : Toutes les requêtes API retournent 503, y compris /auth/sign_in.
Cause : Le mode maintenance est activé côté backend et les routes d'auth ne sont pas exemptées.
Solution :
# Se connecter directement au backend pour désactiver la maintenance
curl -X PUT http://127.0.0.1:7010/admin/system-status \
-H "Cookie: auth_token=VOTRE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_active": true}'
Danger
Si les routes d'auth sont bloquées par la maintenance, aucun admin ne peut se connecter. Corriger le middleware backend pour exempter /auth/sign_in et /auth/callback.
"Mode maintenance" affiché alors que le système est actif¶
Symptôme : Le dashboard admin affiche "MODE MAINTENANCE" même quand is_active: true.
Cause : Le backend retourne les données en camelCase (isActive) mais le frontend lit en snake_case (is_active).
Solution : Vérifier que normalizeStatusData() dans systemStatusService.js gère bien les deux formats :
Résultats de recherche étudiants vides¶
Symptôme : La recherche d'étudiants dans le wizard de génération ne retourne aucun résultat malgré des étudiants existants.
Cause : Le backend retourne { students: [...] } mais le code frontend cherche data.results.
Solution : Vérifier que le code utilise bien :
Erreur 500 sur GET /documents/available-document-types¶
Symptôme : 'User' object has no attribute 'section' dans les logs backend.
Cause : Un admin appelle cette route qui tente d'accéder à request.user.section, mais les admins n'ont pas d'attribut section.
Solution : Passer ?studentEmail=xxx ou ?classe=PGE1 comme query param pour que le backend utilise la section de l'étudiant ou de la classe au lieu de celle de l'utilisateur connecté.
Un admin perd l'accès à une section du menu ou à une action après le déploiement des droits fins¶
Symptôme : Un admin qui pouvait auparavant voir une section de la sidebar (ex. « Statistiques », « Migration ») ou effectuer une action (ex. modifier un modèle de document) se retrouve avec l'entrée de menu disparue, ou un 403 / redirection vers /unauthorized en accédant directement à l'URL.
Cause : Ce n'est pas un problème de rôle. Depuis l'introduction du système de droits fins (epidocs-api PR #25), un admin non-superadmin n'a accès qu'aux pages et actions pour lesquelles il a un droit précis dans rights[] (voir Rôles et permissions — catalogue dynamique, 24 droits à la dernière vérification). Avant ce système, un admin avait par défaut accès à la quasi-totalité du dashboard ; ce n'est plus le cas.
Solution :
- Identifier le droit requis pour la page/action concernée : soit sur
/admin/admins(le sélecteur affiche label + description de chaque droit), soit directementrequiredRightdanssrc/app/admin/components/sidebar.js/requiredRightsdans le composantProtectedRoutede la page. - Aller sur
/admin/admins, ouvrir « Modifier » sur l'admin concerné. - Dans le sélecteur de droits (accordéon par groupe), ouvrir le groupe correspondant (ex. « Rapports » pour
can_view_dashboard_stats) et cocher le droit manquant. - Enregistrer.
Piège fréquent — l'admin a bien le droit en base mais ne le voit toujours pas : GET /auth/me n'est appelé qu'une fois au chargement de l'app (voir frontend.md — Fraîcheur de la session). Si l'admin concerné avait déjà l'app ouverte quand le droit a été accordé, il doit recharger la page ou se reconnecter — ce n'est pas nécessairement un problème d'attribution.
Diagnostic rapide
Comparer les rights[] retournés par une requête fraîche à GET /auth/me (pas depuis un onglet déjà ouvert) pour cet admin avec le catalogue (GET /admin/rights). Un droit absent = fonctionnalité inaccessible, quel que soit le rôle (admin ou superadmin fait exception : un superadmin a toujours accès à tout, à condition que son rights[] soit à jour — même piège de fraîcheur de session possible).
Un nouveau droit backend n'apparaît pas dans le sélecteur de /admin/admins¶
Symptôme historique (avant le 2026-09-09) : un droit existait côté backend (ex. can_manage_attendance) mais aucune case à cocher correspondante n'existait sur /admin/admins — impossible de l'accorder depuis l'UI, il fallait le positionner directement en base.
Cause (résolue) : le catalogue de droits était une liste RIGHTS_OPTIONS/RIGHT_GROUPS hardcodée dans src/app/admin/admins/page.js, qu'il fallait mettre à jour et redéployer à chaque nouveau droit — et ça a été oublié pour can_manage_attendance puis, une seconde fois, pour can_edit_student_info/can_manage_data_integrity avant d'être enfin corrigé structurellement.
Ce bug de classe ne peut plus se produire depuis le 2026-09-09 : le catalogue est chargé dynamiquement via GET /admin/rights (voir frontend.md). Un nouveau droit backend apparaît automatiquement, avec un label générique de repli si le backend ne lui a pas encore assigné de label/description/group.
Si malgré tout un droit backend n'apparaît toujours pas : ce n'est plus un problème de code frontend à corriger — vérifier plutôt que GET /admin/rights le renvoie bien (curl authentifié, ou onglet réseau sur /admin/admins), et si non, que le droit existe bien dans la table Right côté backend.
Une page admin est accessible par URL directe malgré un requiredRight non satisfait dans la sidebar¶
Symptôme : un admin sans le droit attendu ne voit pas le lien dans la sidebar, mais peut quand même charger la page complète (et ses données) en tapant l'URL directement — aucun 403, aucune redirection vers /unauthorized.
Cause : requiredRight sur un item de sidebar.js ne fait que masquer le lien — ce n'est pas un contrôle d'accès. Seul un <ProtectedRoute requiredRights={[...]}> enveloppant le contenu de page.js protège réellement la page. Incident réel (corrigé le 2026-09-09) : les 6 pages /admin/attendance/* n'avaient aucun ProtectedRoute, voir attendance.md — Protection des pages.
Solution : ajouter <ProtectedRoute requiredRights={[...]}> autour du contenu du page.js concerné, avec les mêmes droits que ceux déclarés dans sidebar.js.
À auditer sur toute page admin qui semble ancienne ou jamais revue
Il n'existe aucun test automatisé qui détecte une page sans ProtectedRoute. Si une nouvelle page admin est ajoutée (ou si on audite l'existant), vérifier manuellement — grep -L ProtectedRoute src/app/admin/**/page.js donne rapidement la liste des pages sans le composant, à croiser avec celles qui devraient en avoir un d'après sidebar.js.
fetch() sur une URL cross-origin renvoie 401 malgré une session valide (image, fichier à télécharger)¶
Symptôme : un endpoint renvoie une URL absolue vers api.mydocs.epitools.bj (ou api-mydocs.epitools.bj) à consommer directement (ex. qrCodeUrl), l'affichage via <Image src=...>/<img> fonctionne, mais un bouton "Télécharger" qui fait lui-même fetch(url) échoue avec 401.
Cause : fetch() a pour défaut credentials: 'same-origin' — il n'envoie pas le cookie de session pour une requête cross-origin, contrairement à un <img> classique (pas soumis à cette règle) ou à axiosInstance (withCredentials: true global). Détail complet et code exact du fix : attendance.md — QR codes.
Solution : ne jamais utiliser fetch() nu pour retélécharger une ressource protégée par cookie cross-origin — passer par axiosInstance.get(url, { responseType: "blob" }) (accepte une URL absolue, ignore sa propre baseURL dans ce cas), ou à défaut ajouter credentials: 'include' à l'appel fetch().
Avertissement navigateur « Encountered two children with the same key » sur un tableau agrégé¶
Symptôme : la console affiche Warning: Encountered two children with the same key (souvent null ou une petite valeur numérique) sur un tableau construit à partir d'une agrégation backend (ex. stats par agent/chaperon).
Cause : le champ utilisé comme clé React (key={item.someId}) n'est pas garanti unique — null pour plusieurs entrées sans identifiant individuel (ex. plusieurs admins distincts sans SecurityGuard), ou dupliqué si une même entité peut apparaître dans plus d'une ligne agrégée.
Solution : utiliser une clé composite inspirée d'au moins deux champs distinctifs, avec l'index du tableau en filet de sécurité final, ex. key={`${item.id ?? "none"}-${item.role}-${idx}`}. Voir l'incident concret sur /admin/attendance/scanners dans attendance.md.
Backend¶
TypeError sur PUT /admin/system-status¶
Symptôme : TypeError: '<=' not supported between instances of 'str' and 'datetime.datetime'
Cause : Les champs scheduled_start et scheduled_end sont stockés comme strings mais comparés avec timezone.now() (datetime).
Solution : Parser les dates avec parse_datetime() avant la comparaison dans la vue backend.
Génération en lot timeout¶
Symptôme : La requête POST /documents/admin/batch-generate timeout pour les grandes classes.
Cause : La génération de nombreux PDFs prend du temps. Le timeout par défaut d'Axios est trop court.
Solution : Le frontend utilise déjà un timeout de 300 secondes (5 minutes). Si ce n'est pas suffisant :
# Vérifier côté Nginx
# /etc/nginx/sites-available/epidocs
proxy_read_timeout 600s;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
Cookie auth_token non défini après login¶
Symptôme : Le login réussit (200 OK) mais le cookie n'est pas stocké dans le navigateur.
Cause : Le Set-Cookie du backend a un attribut Domain incompatible avec le frontend.
Solution :
- Dev :
AUTH_COOKIE_DOMAIN = Nonedans les settings Django - Prod :
AUTH_COOKIE_DOMAIN = ".epitools.bj"(domaine racine avec point)