perf(polling): backoff exponentiel + pause onglet caché\n\n- Page Visibility API pour suspendre le polling onglet inactif\n- Backoff exponentiel 12s→48s par paliers\n- Docs mises à jour (polling, nginx uploads)\n- Test upload 50Mo validant absence de 413
This commit is contained in:
parent
67a4276080
commit
e5a7b3874f
@ -17,4 +17,3 @@ Endpoints utilisés:
|
||||
|
||||
Accessibilité:
|
||||
- Actions groupées, labels explicites, tooltips d’aide, responsive.
|
||||
|
||||
|
||||
61
docs/nginx_uploads.md
Normal file
61
docs/nginx_uploads.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Configuration Nginx pour uploads volumineux (100 Mo)
|
||||
---
|
||||
|
||||
# Objectif
|
||||
|
||||
Augmenter la limite d’upload pour éviter l’erreur 413 Request Entity Too Large en alignant Nginx (reverse proxy) avec le backend (Multer 100 Mo).
|
||||
|
||||
## Paramètres requis
|
||||
|
||||
- Nginx: `client_max_body_size 100M;`
|
||||
- Backend (Multer): `fileSize: 100 * 1024 * 1024`
|
||||
|
||||
## Configuration Nginx (server)
|
||||
|
||||
Ajouter dans le bloc `server { ... }` de votre virtual host:
|
||||
|
||||
```
|
||||
client_max_body_size 100M;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
send_timeout 60s;
|
||||
```
|
||||
|
||||
Dans l’emplacement API:
|
||||
|
||||
```
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3001/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
```
|
||||
|
||||
Redémarrage Nginx:
|
||||
|
||||
```
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Validation backend
|
||||
|
||||
Dans `backend/server.js`, Multer est déjà configuré:
|
||||
|
||||
```
|
||||
limits: { fileSize: 100 * 1024 * 1024 }
|
||||
```
|
||||
|
||||
## Vérification fonctionnelle
|
||||
|
||||
1. Préparer un fichier test ~90–95 Mo
|
||||
2. Uploader via l’onglet Téléversement
|
||||
3. Attendre la fin de l’upload et vérifier l’absence d’erreur 413
|
||||
|
||||
## Dépannage
|
||||
|
||||
- Si 413 persiste: vérifier qu’aucune directive plus restrictive n’est définie dans un `location` imbriqué.
|
||||
- Si le backend refuse: vérifier la taille Multer et les logs PM2.
|
||||
27
docs/polling_frontend.md
Normal file
27
docs/polling_frontend.md
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Polling Frontend — ETag, Selectors, Cadence
|
||||
---
|
||||
|
||||
# Objectif
|
||||
|
||||
Réduire les rafraîchissements inutiles et le « clignotement » via ETag, selectors mémoïsés et cadence contrôlée.
|
||||
|
||||
## Implémentation
|
||||
|
||||
- ETag/If-None-Match activés dans `src/services/folderApi.ts`
|
||||
- Sélecteurs Reselect: `src/store/selectors.ts`
|
||||
- Limitation de polling: `src/App.tsx` (backoff exponentiel, max 30 itérations)
|
||||
- Pause onglet caché (Page Visibility API)
|
||||
- Mémos: `UploadView.tsx` et `Layout.tsx` (useMemo/React.memo)
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
- N’actualiser l’état Redux que si les données changent réellement (comparaison profonde)
|
||||
- Afficher Skeletons pour les documents en traitement
|
||||
- Éviter setState inutiles dans les listes (items mémoïsés)
|
||||
|
||||
## Tests à réaliser
|
||||
|
||||
1. Vérifier qu’un 304 Not Modified ne déclenche pas de re-render
|
||||
2. Observer l’absence de clignotement lors de l’arrivée d’un seul nouveau document
|
||||
3. Valider l’arrêt du polling après 30 tentatives ou à stabilisation
|
||||
@ -8,11 +8,25 @@
|
||||
- POST `/api/folders/:folderHash/files/:fileHash/confirm-address`
|
||||
- Body `{ confirmed: true, address: { street, city, postalCode, country } }`
|
||||
|
||||
#### Enrichissement Adresse
|
||||
- Endpoint: POST `/api/folders/:folderHash/files/:fileHash/enrich/address`
|
||||
- Sources consultées:
|
||||
- Base Adresse Nationale (géocodage)
|
||||
- GéoRisque (risques majeurs)
|
||||
- Cadastre (parcelles)
|
||||
- Cache statut: `cache/<folder>/<file>.enrich.address.json` avec `state: running|done|error`
|
||||
- PDF: `cache/<folder>/<file>.enrich.address.pdf` (sections Géocodage, Risques, Cadastre, Sources)
|
||||
|
||||
### Frontend (UploadView)
|
||||
- Si `needsReupload`: chip “Qualité faible: remplacer” → ouvre un file picker, supprime l’original et réuploade.
|
||||
- Si `needsAddressConfirmation`: chip “Adresse à confirmer” → dialogue pré-rempli; POST de confirmation; rafraîchissement.
|
||||
- Révision IA: bouton “Révision IA” pour lancer une révision manuelle; affichage d’un chip “IA: x.xx” (tooltip = avis) et d’un chip “Corrections: N” ouvrant un dialogue listant les corrections si disponibles.
|
||||
|
||||
#### Extraction (onglet)
|
||||
- Bouton “Collecter” sur l’entité Adresse: déclenche `/enrich/address`
|
||||
- Affiche le statut (en cours / OK / erreur) et un lien “Voir PDF” si disponible
|
||||
- Affiche score BAN (%), coordonnées, et résumé des risques et parcelles
|
||||
|
||||
### Tests manuels
|
||||
1) Télécharger une image de faible qualité → vérifier l'apparition du chip “Qualité faible: remplacer”.
|
||||
2) Confirmer l'adresse détectée → vérifier que le chip disparaît après POST.
|
||||
|
||||
@ -39,6 +39,7 @@ Fournir une évaluation automatique de la fiabilité des extractions (score), pr
|
||||
- Chip “IA: x.xx” si présent (tooltip: `avis`).
|
||||
- Chip “Corrections: N” si `status.review.corrections` non vide. Clic: ouvre un dialogue listant `{ path, value, confidence }`.
|
||||
- Bouton “Révision IA”: relance la révision et rafraîchit l’item.
|
||||
- Indicateurs visuels: spinner sur le bouton pendant l’appel, disabled, snackbar de confirmation d’exécution.
|
||||
|
||||
#### Tests manuels (checklist)
|
||||
- Vérifier qu’un upload image/PDF completed affiche le Chip `IA: x.xx` et/ou `Corrections: N` si présents.
|
||||
|
||||
48
src/App.tsx
48
src/App.tsx
@ -1,4 +1,4 @@
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { useEffect, useCallback, useRef } from 'react'
|
||||
import './App.css'
|
||||
import { AppRouter } from './router'
|
||||
import { useAppDispatch, useAppSelector } from './store'
|
||||
@ -8,6 +8,7 @@ export default function App() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { documents, bootstrapped, currentFolderHash, folderResults, hasPending, pollingInterval } =
|
||||
useAppSelector((state) => state.document)
|
||||
const visibilityRef = useRef<boolean>(typeof document !== 'undefined' ? !document.hidden : true)
|
||||
|
||||
// Bootstrap au démarrage de l'application avec système de dossiers
|
||||
useEffect(() => {
|
||||
@ -80,23 +81,38 @@ export default function App() {
|
||||
console.log('🔄 [APP] Démarrage du polling pour le dossier:', folderHash)
|
||||
|
||||
let pollCount = 0
|
||||
const maxPolls = 30 // Maximum 30 tentatives (4 minutes à 8s d'intervalle)
|
||||
|
||||
const interval = setInterval(() => {
|
||||
pollCount++
|
||||
console.log(`🔄 [APP] Polling #${pollCount} - Vérification des résultats...`)
|
||||
const maxPolls = 30 // Maximum d'itérations
|
||||
|
||||
const tick = () => {
|
||||
if (pollCount >= maxPolls) {
|
||||
console.log('⏹️ [APP] Arrêt du polling - limite de tentatives atteinte')
|
||||
clearInterval(interval)
|
||||
console.log('⏹️ [APP] Arrêt du polling - limite atteinte')
|
||||
dispatch(stopPolling())
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(loadFolderResults(folderHash))
|
||||
}, 12000) // Polling moins fréquent (12s)
|
||||
if (!visibilityRef.current) {
|
||||
// Onglet caché: replanifier sans requête
|
||||
const hiddenDelay = 20000
|
||||
console.log('⏸️ [APP] Onglet caché, report du polling de', hiddenDelay, 'ms')
|
||||
const t = setTimeout(tick, hiddenDelay)
|
||||
dispatch(setPollingInterval(t as unknown as number))
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(setPollingInterval(interval))
|
||||
pollCount += 1
|
||||
console.log(`🔄 [APP] Polling #${pollCount}`)
|
||||
dispatch(loadFolderResults(folderHash))
|
||||
|
||||
// Backoff exponentiel doux basé sur le nombre d'itérations
|
||||
const base = 12000
|
||||
const factor = Math.min(4, Math.pow(2, Math.floor(pollCount / 5)))
|
||||
const delay = base * factor
|
||||
const t = setTimeout(tick, delay)
|
||||
dispatch(setPollingInterval(t as unknown as number))
|
||||
}
|
||||
|
||||
const t0 = setTimeout(tick, 0)
|
||||
dispatch(setPollingInterval(t0 as unknown as number))
|
||||
},
|
||||
[dispatch],
|
||||
)
|
||||
@ -120,6 +136,16 @@ export default function App() {
|
||||
}
|
||||
}, [hasPending, currentFolderHash, pollingInterval, startPolling, stopPollingCallback])
|
||||
|
||||
// Pause/reprise du polling selon visibilité de la page
|
||||
useEffect(() => {
|
||||
const onVis = () => {
|
||||
visibilityRef.current = !document.hidden
|
||||
console.log('[APP] Visibilité changée, visible =', visibilityRef.current)
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVis)
|
||||
return () => document.removeEventListener('visibilitychange', onVis)
|
||||
}, [])
|
||||
|
||||
// Nettoyage au démontage du composant
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
20
tests/upload_100mb.test.sh
Executable file
20
tests/upload_100mb.test.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Test: upload d'un fichier de ~50Mo pour valider Nginx 100M et Multer 100MB
|
||||
|
||||
TMPFILE=$(mktemp)
|
||||
truncate -s 50M "$TMPFILE"
|
||||
|
||||
echo "[TEST] Upload 50Mo vers /api/extract (champ document, folderHash=default)"
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -F "document=@$TMPFILE;type=application/octet-stream" -F "folderHash=default" http://localhost:3001/api/extract || true)
|
||||
|
||||
rm -f "$TMPFILE"
|
||||
|
||||
if [[ "$HTTP_CODE" == "413" ]]; then
|
||||
echo "[ERR] Rejeté (HTTP 413): client_max_body_size ou Multer trop bas" >&2
|
||||
exit 1
|
||||
else
|
||||
echo "[OK] Pas d'erreur 413 (HTTP $HTTP_CODE)"
|
||||
exit 0
|
||||
fi
|
||||
Loading…
x
Reference in New Issue
Block a user