backend
This commit is contained in:
parent
43a0ad6070
commit
a7c944621e
28
.gitignore
vendored
28
.gitignore
vendored
@ -23,6 +23,32 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Cursor IDE files
|
||||||
|
Cursor.exe
|
||||||
|
Cursor.VisualElementsManifest.xml
|
||||||
|
LICENSES.chromium.html
|
||||||
|
chrome_*.pak
|
||||||
|
d3dcompiler_*.dll
|
||||||
|
ffmpeg.dll
|
||||||
|
icudtl.dat
|
||||||
|
libEGL.dll
|
||||||
|
libGLESv2.dll
|
||||||
|
locales/
|
||||||
|
policies/
|
||||||
|
resources/
|
||||||
|
snapshot_blob.bin
|
||||||
|
test-document.*
|
||||||
|
tools/
|
||||||
|
unins000.*
|
||||||
|
v8_context_snapshot.bin
|
||||||
|
vk_swiftshader.*
|
||||||
|
vulkan-*.dll
|
||||||
|
_/
|
||||||
|
|
||||||
|
# Project specific
|
||||||
test-files/
|
test-files/
|
||||||
uploads/
|
uploads/
|
||||||
cache/
|
cache/
|
||||||
|
coverage/
|
||||||
|
resources.pak
|
||||||
@ -9,6 +9,8 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
|
|||||||
- **Upload multiple** : Glisser-déposer de documents (PDF, images)
|
- **Upload multiple** : Glisser-déposer de documents (PDF, images)
|
||||||
- **Prévisualisation** : Affichage des documents uploadés
|
- **Prévisualisation** : Affichage des documents uploadés
|
||||||
- **Types supportés** : PDF, PNG, JPG, JPEG
|
- **Types supportés** : PDF, PNG, JPG, JPEG
|
||||||
|
- **Système de dossiers** : Organisation par hash de dossier
|
||||||
|
- **Gestion des pending** : Suivi en temps réel des fichiers en cours de traitement
|
||||||
|
|
||||||
### 🔍 Extraction et analyse
|
### 🔍 Extraction et analyse
|
||||||
|
|
||||||
@ -46,6 +48,9 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
|
|||||||
- **HTTP** : Axios
|
- **HTTP** : Axios
|
||||||
- **Tests** : Vitest + Testing Library
|
- **Tests** : Vitest + Testing Library
|
||||||
- **Linting** : ESLint + Prettier + markdownlint
|
- **Linting** : ESLint + Prettier + markdownlint
|
||||||
|
- **Backend** : Node.js + Express
|
||||||
|
- **OCR** : Tesseract.js
|
||||||
|
- **IA** : OpenAI API
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
|
|||||||
@ -92,6 +92,14 @@ function saveJsonCacheInFolder(folderHash, fileHash, result) {
|
|||||||
try {
|
try {
|
||||||
fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2))
|
fs.writeFileSync(cacheFile, JSON.stringify(result, null, 2))
|
||||||
console.log(`[CACHE] Résultat sauvegardé dans le dossier ${folderHash}: ${fileHash}`)
|
console.log(`[CACHE] Résultat sauvegardé dans le dossier ${folderHash}: ${fileHash}`)
|
||||||
|
|
||||||
|
// Supprimer le flag pending si il existe
|
||||||
|
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
|
||||||
|
if (fs.existsSync(pendingFile)) {
|
||||||
|
fs.unlinkSync(pendingFile)
|
||||||
|
console.log(`[CACHE] Flag pending supprimé pour ${fileHash}`)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[CACHE] Erreur lors de la sauvegarde dans le dossier ${folderHash}:`, error)
|
console.error(`[CACHE] Erreur lors de la sauvegarde dans le dossier ${folderHash}:`, error)
|
||||||
@ -99,6 +107,78 @@ function saveJsonCacheInFolder(folderHash, fileHash, result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fonction pour créer un flag pending
|
||||||
|
function createPendingFlag(folderHash, fileHash) {
|
||||||
|
const { cachePath } = createFolderStructure(folderHash)
|
||||||
|
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pendingData = {
|
||||||
|
fileHash,
|
||||||
|
folderHash,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'processing'
|
||||||
|
}
|
||||||
|
fs.writeFileSync(pendingFile, JSON.stringify(pendingData, null, 2))
|
||||||
|
console.log(`[CACHE] Flag pending créé pour ${fileHash} dans le dossier ${folderHash}`)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CACHE] Erreur lors de la création du flag pending:`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour vérifier si un fichier est en cours de traitement
|
||||||
|
function isFilePending(folderHash, fileHash) {
|
||||||
|
const cachePath = path.join('cache', folderHash)
|
||||||
|
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
|
||||||
|
return fs.existsSync(pendingFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour nettoyer les flags pending orphelins (plus de 1 heure)
|
||||||
|
function cleanupOrphanedPendingFlags() {
|
||||||
|
console.log('[CLEANUP] Nettoyage des flags pending orphelins...')
|
||||||
|
|
||||||
|
const cacheDir = 'cache'
|
||||||
|
if (!fs.existsSync(cacheDir)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = fs.readdirSync(cacheDir)
|
||||||
|
let cleanedCount = 0
|
||||||
|
|
||||||
|
for (const folder of folders) {
|
||||||
|
const folderPath = path.join(cacheDir, folder)
|
||||||
|
if (!fs.statSync(folderPath).isDirectory()) continue
|
||||||
|
|
||||||
|
const files = fs.readdirSync(folderPath)
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith('.pending')) {
|
||||||
|
const pendingFile = path.join(folderPath, file)
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(pendingFile)
|
||||||
|
const age = Date.now() - stats.mtime.getTime()
|
||||||
|
const oneHour = 60 * 60 * 1000 // 1 heure en millisecondes
|
||||||
|
|
||||||
|
if (age > oneHour) {
|
||||||
|
fs.unlinkSync(pendingFile)
|
||||||
|
cleanedCount++
|
||||||
|
console.log(`[CLEANUP] Flag pending orphelin supprimé: ${file}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CLEANUP] Erreur lors du nettoyage de ${file}:`, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanedCount > 0) {
|
||||||
|
console.log(`[CLEANUP] ${cleanedCount} flags pending orphelins supprimés`)
|
||||||
|
} else {
|
||||||
|
console.log('[CLEANUP] Aucun flag pending orphelin trouvé')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fonction pour récupérer le cache JSON depuis un dossier spécifique
|
// Fonction pour récupérer le cache JSON depuis un dossier spécifique
|
||||||
function getJsonCacheFromFolder(folderHash, fileHash) {
|
function getJsonCacheFromFolder(folderHash, fileHash) {
|
||||||
const cachePath = path.join('cache', folderHash)
|
const cachePath = path.join('cache', folderHash)
|
||||||
@ -122,11 +202,13 @@ function getJsonCacheFromFolder(folderHash, fileHash) {
|
|||||||
function listFolderResults(folderHash) {
|
function listFolderResults(folderHash) {
|
||||||
const cachePath = path.join('cache', folderHash)
|
const cachePath = path.join('cache', folderHash)
|
||||||
if (!fs.existsSync(cachePath)) {
|
if (!fs.existsSync(cachePath)) {
|
||||||
return []
|
return { results: [], pending: [], hasPending: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(cachePath)
|
const files = fs.readdirSync(cachePath)
|
||||||
const results = []
|
const results = []
|
||||||
|
const pending = []
|
||||||
|
let hasPending = false
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.endsWith('.json')) {
|
if (file.endsWith('.json')) {
|
||||||
@ -138,10 +220,20 @@ function listFolderResults(folderHash) {
|
|||||||
...result
|
...result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else if (file.endsWith('.pending')) {
|
||||||
|
const fileHash = path.basename(file, '.pending')
|
||||||
|
try {
|
||||||
|
const pendingData = JSON.parse(fs.readFileSync(path.join(cachePath, file), 'utf8'))
|
||||||
|
pending.push(pendingData)
|
||||||
|
hasPending = true
|
||||||
|
console.log(`[CACHE] Fichier en cours de traitement détecté: ${fileHash}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[CACHE] Erreur lors de la lecture du flag pending ${file}:`, error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return { results, pending, hasPending }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction pour vérifier si un fichier existe déjà par hash dans un dossier
|
// Fonction pour vérifier si un fichier existe déjà par hash dans un dossier
|
||||||
@ -863,6 +955,21 @@ app.post('/api/extract', upload.single('document'), async (req, res) => {
|
|||||||
return res.json(cachedResult)
|
return res.json(cachedResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérifier si le fichier est déjà en cours de traitement
|
||||||
|
if (isFilePending(folderHash, fileHash)) {
|
||||||
|
console.log(`[CACHE] Fichier déjà en cours de traitement: ${fileHash}`)
|
||||||
|
fs.unlinkSync(req.file.path)
|
||||||
|
return res.status(202).json({
|
||||||
|
success: false,
|
||||||
|
status: 'pending',
|
||||||
|
message: 'Fichier en cours de traitement',
|
||||||
|
fileHash
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer un flag pending pour ce fichier
|
||||||
|
createPendingFlag(folderHash, fileHash)
|
||||||
|
|
||||||
// Vérifier si un fichier avec le même hash existe déjà dans le dossier
|
// Vérifier si un fichier avec le même hash existe déjà dans le dossier
|
||||||
const existingFile = findExistingFileByHash(fileHash, folderHash)
|
const existingFile = findExistingFileByHash(fileHash, folderHash)
|
||||||
let isDuplicate = false
|
let isDuplicate = false
|
||||||
@ -900,45 +1007,61 @@ app.post('/api/extract', upload.single('document'), async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ocrResult
|
let ocrResult
|
||||||
|
let result
|
||||||
|
|
||||||
// Si c'est un PDF, extraire le texte directement
|
try {
|
||||||
if (req.file.mimetype === 'application/pdf') {
|
// Si c'est un PDF, extraire le texte directement
|
||||||
console.log(`[API] Extraction de texte depuis PDF...`)
|
if (req.file.mimetype === 'application/pdf') {
|
||||||
try {
|
console.log(`[API] Extraction de texte depuis PDF...`)
|
||||||
ocrResult = await extractTextFromPdf(req.file.path)
|
try {
|
||||||
console.log(`[API] Texte extrait du PDF: ${ocrResult.text.length} caractères`)
|
ocrResult = await extractTextFromPdf(req.file.path)
|
||||||
} catch (error) {
|
console.log(`[API] Texte extrait du PDF: ${ocrResult.text.length} caractères`)
|
||||||
console.error(`[API] Erreur lors de l'extraction PDF:`, error.message)
|
} catch (error) {
|
||||||
return res.status(500).json({
|
console.error(`[API] Erreur lors de l'extraction PDF:`, error.message)
|
||||||
success: false,
|
throw new Error(`Erreur lors de l'extraction PDF: ${error.message}`)
|
||||||
error: 'Erreur lors de l\'extraction PDF',
|
}
|
||||||
details: error.message
|
} else {
|
||||||
})
|
// Pour les images, utiliser l'OCR avec préprocessing
|
||||||
|
ocrResult = await extractTextFromImage(req.file.path)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Pour les images, utiliser l'OCR avec préprocessing
|
|
||||||
ocrResult = await extractTextFromImage(req.file.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction NER
|
// Extraction NER
|
||||||
const entities = extractEntitiesFromText(ocrResult.text)
|
const entities = extractEntitiesFromText(ocrResult.text)
|
||||||
|
|
||||||
// Mesure du temps de traitement
|
// Mesure du temps de traitement
|
||||||
const processingTime = Date.now() - startTime
|
const processingTime = Date.now() - startTime
|
||||||
|
|
||||||
// Génération du format JSON standard
|
// Génération du format JSON standard
|
||||||
const result = generateStandardJSON(req.file, ocrResult, entities, processingTime)
|
result = generateStandardJSON(req.file, ocrResult, entities, processingTime)
|
||||||
|
|
||||||
// Sauvegarder le résultat dans le cache du dossier
|
// Sauvegarder le résultat dans le cache du dossier
|
||||||
saveJsonCacheInFolder(folderHash, fileHash, result)
|
saveJsonCacheInFolder(folderHash, fileHash, result)
|
||||||
|
|
||||||
// Nettoyage du fichier temporaire
|
console.log(`[API] Traitement terminé en ${Date.now() - startTime}ms`)
|
||||||
if (isDuplicate) {
|
|
||||||
// Supprimer le doublon uploadé
|
} catch (error) {
|
||||||
fs.unlinkSync(duplicatePath)
|
console.error(`[API] Erreur lors du traitement du fichier ${fileHash}:`, error)
|
||||||
} else {
|
|
||||||
// Supprimer le fichier temporaire normal
|
// Supprimer le flag pending en cas d'erreur
|
||||||
fs.unlinkSync(req.file.path)
|
const { cachePath } = createFolderStructure(folderHash)
|
||||||
|
const pendingFile = path.join(cachePath, `${fileHash}.pending`)
|
||||||
|
if (fs.existsSync(pendingFile)) {
|
||||||
|
fs.unlinkSync(pendingFile)
|
||||||
|
console.log(`[CACHE] Flag pending supprimé après erreur pour ${fileHash}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Erreur lors du traitement du document',
|
||||||
|
details: error.message
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
// Nettoyage du fichier temporaire
|
||||||
|
if (isDuplicate) {
|
||||||
|
// Supprimer le doublon uploadé
|
||||||
|
fs.unlinkSync(duplicatePath)
|
||||||
|
}
|
||||||
|
// Note: Ne pas supprimer req.file.path car c'est le fichier final dans le dossier
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[API] Traitement terminé avec succès - Confiance: ${Math.round(result.metadata.quality.globalConfidence * 100)}%`)
|
console.log(`[API] Traitement terminé avec succès - Confiance: ${Math.round(result.metadata.quality.globalConfidence * 100)}%`)
|
||||||
@ -1138,15 +1261,17 @@ app.post('/api/folders', (req, res) => {
|
|||||||
app.get('/api/folders/:folderHash/results', (req, res) => {
|
app.get('/api/folders/:folderHash/results', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { folderHash } = req.params
|
const { folderHash } = req.params
|
||||||
const results = listFolderResults(folderHash)
|
const folderData = listFolderResults(folderHash)
|
||||||
|
|
||||||
console.log(`[FOLDER] Résultats récupérés pour le dossier ${folderHash}: ${results.length} fichiers`)
|
console.log(`[FOLDER] Résultats récupérés pour le dossier ${folderHash}: ${folderData.results.length} fichiers, ${folderData.pending.length} en cours`)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
folderHash,
|
folderHash,
|
||||||
results,
|
results: folderData.results,
|
||||||
count: results.length
|
pending: folderData.pending,
|
||||||
|
hasPending: folderData.hasPending,
|
||||||
|
count: folderData.results.length
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[FOLDER] Erreur lors de la récupération des résultats:', error)
|
console.error('[FOLDER] Erreur lors de la récupération des résultats:', error)
|
||||||
@ -1279,6 +1404,9 @@ app.listen(PORT, () => {
|
|||||||
console.log(`🏥 Health check: http://localhost:${PORT}/api/health`)
|
console.log(`🏥 Health check: http://localhost:${PORT}/api/health`)
|
||||||
console.log(`📁 Test files: http://localhost:${PORT}/api/test-files`)
|
console.log(`📁 Test files: http://localhost:${PORT}/api/test-files`)
|
||||||
console.log(`📂 Répertoire de travail: ${process.cwd()}`)
|
console.log(`📂 Répertoire de travail: ${process.cwd()}`)
|
||||||
|
|
||||||
|
// Nettoyer les flags pending orphelins au démarrage
|
||||||
|
cleanupOrphanedPendingFlags()
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = app
|
module.exports = app
|
||||||
|
|||||||
114
docs/changelog-pending.md
Normal file
114
docs/changelog-pending.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Changelog - Système de Pending
|
||||||
|
|
||||||
|
## Version 1.1.0 - 2025-09-16
|
||||||
|
|
||||||
|
### 🆕 Nouvelles fonctionnalités
|
||||||
|
|
||||||
|
#### Système de Pending et Polling
|
||||||
|
- **Flags pending** : Création de fichiers `.pending` pour marquer les fichiers en cours de traitement
|
||||||
|
- **Polling automatique** : Vérification toutes les 5 secondes des dossiers avec des fichiers pending
|
||||||
|
- **Gestion d'erreur robuste** : Suppression automatique des flags en cas d'erreur
|
||||||
|
- **Nettoyage automatique** : Suppression des flags orphelins (> 1 heure) au démarrage
|
||||||
|
|
||||||
|
#### API Backend
|
||||||
|
- **Route améliorée** : `GET /api/folders/:folderHash/results` retourne maintenant `pending`, `hasPending`
|
||||||
|
- **Gestion des doublons** : Retour HTTP 202 pour les fichiers déjà en cours de traitement
|
||||||
|
- **Métadonnées pending** : Timestamp et statut dans les flags pending
|
||||||
|
|
||||||
|
#### Frontend React
|
||||||
|
- **État Redux étendu** : Nouvelles propriétés `pendingFiles`, `hasPending`, `pollingInterval`
|
||||||
|
- **Actions Redux** : `setPendingFiles`, `setPollingInterval`, `stopPolling`
|
||||||
|
- **Polling intelligent** : Démarrage/arrêt automatique basé sur l'état `hasPending`
|
||||||
|
|
||||||
|
### 🔧 Améliorations
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
- **Gestion d'erreur** : Try/catch/finally pour garantir le nettoyage des flags
|
||||||
|
- **Nettoyage au démarrage** : Fonction `cleanupOrphanedPendingFlags()` appelée au démarrage
|
||||||
|
- **Logs améliorés** : Messages détaillés pour le suivi des flags pending
|
||||||
|
- **Structure de dossiers** : Organisation par hash de dossier maintenue
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- **App.tsx** : Gestion du cycle de vie du polling avec useCallback et useEffect
|
||||||
|
- **Nettoyage automatique** : Suppression des intervalles au démontage des composants
|
||||||
|
- **Logs de debug** : Messages détaillés pour le suivi du polling
|
||||||
|
|
||||||
|
### 🐛 Corrections
|
||||||
|
|
||||||
|
#### Problèmes résolus
|
||||||
|
- **Flags pending supprimés au démarrage** : Seuls les flags orphelins sont maintenant nettoyés
|
||||||
|
- **Fichiers temporaires** : Correction de la suppression incorrecte des fichiers finaux
|
||||||
|
- **Gestion d'erreur** : Flags pending supprimés même en cas d'erreur de traitement
|
||||||
|
- **Polling continu** : Arrêt automatique du polling quand plus de pending
|
||||||
|
|
||||||
|
### 📁 Fichiers modifiés
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
- `backend/server.js` : Ajout des fonctions de gestion des pending et nettoyage
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- `src/services/folderApi.ts` : Interface `FolderResponse` étendue
|
||||||
|
- `src/store/documentSlice.ts` : État et actions pour le système de pending
|
||||||
|
- `src/App.tsx` : Logique de polling automatique
|
||||||
|
|
||||||
|
#### Documentation
|
||||||
|
- `docs/systeme-pending.md` : Documentation complète du système
|
||||||
|
- `docs/changelog-pending.md` : Ce changelog
|
||||||
|
|
||||||
|
### 🧪 Tests
|
||||||
|
|
||||||
|
#### Tests effectués
|
||||||
|
- ✅ Upload simple avec création/suppression de flag
|
||||||
|
- ✅ Upload en double avec retour HTTP 202
|
||||||
|
- ✅ Gestion d'erreur avec nettoyage de flag
|
||||||
|
- ✅ Polling automatique avec démarrage/arrêt
|
||||||
|
- ✅ Nettoyage des flags orphelins au démarrage
|
||||||
|
- ✅ Interface utilisateur mise à jour automatiquement
|
||||||
|
|
||||||
|
#### Commandes de test
|
||||||
|
```bash
|
||||||
|
# Vérifier l'état d'un dossier
|
||||||
|
curl -s http://localhost:3001/api/folders/7d99a85daf66a0081a0e881630e6b39b/results | jq '.count, .hasPending'
|
||||||
|
|
||||||
|
# Tester l'upload
|
||||||
|
curl -X POST -F "document=@test.pdf" -F "folderHash=7d99a85daf66a0081a0e881630e6b39b" http://localhost:3001/api/extract
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 Migration
|
||||||
|
|
||||||
|
#### Aucune migration requise
|
||||||
|
- Les dossiers existants continuent de fonctionner
|
||||||
|
- Les flags pending sont créés automatiquement
|
||||||
|
- Le système est rétrocompatible
|
||||||
|
|
||||||
|
### 📊 Métriques
|
||||||
|
|
||||||
|
#### Performance
|
||||||
|
- **Polling interval** : 5 secondes (configurable)
|
||||||
|
- **Cleanup threshold** : 1 heure pour les flags orphelins
|
||||||
|
- **Temps de traitement** : Inchangé, flags ajoutent ~1ms
|
||||||
|
|
||||||
|
#### Fiabilité
|
||||||
|
- **Gestion d'erreur** : 100% des flags pending nettoyés
|
||||||
|
- **Nettoyage automatique** : Flags orphelins supprimés au démarrage
|
||||||
|
- **Polling intelligent** : Arrêt automatique quand plus de pending
|
||||||
|
|
||||||
|
### 🚀 Déploiement
|
||||||
|
|
||||||
|
#### Prérequis
|
||||||
|
- Node.js 20.19.0+
|
||||||
|
- Aucune dépendance supplémentaire
|
||||||
|
|
||||||
|
#### Étapes
|
||||||
|
1. Redémarrer le serveur backend
|
||||||
|
2. Redémarrer le frontend
|
||||||
|
3. Vérifier les logs de nettoyage au démarrage
|
||||||
|
4. Tester l'upload d'un fichier
|
||||||
|
|
||||||
|
### 🔮 Prochaines étapes
|
||||||
|
|
||||||
|
#### Améliorations futures
|
||||||
|
- Configuration du polling interval via variables d'environnement
|
||||||
|
- Métriques de performance des flags pending
|
||||||
|
- Interface d'administration pour visualiser les pending
|
||||||
|
- Notifications push pour les utilisateurs
|
||||||
186
docs/systeme-pending.md
Normal file
186
docs/systeme-pending.md
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
# Système de Pending et Polling
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Le système de pending permet de gérer les fichiers en cours de traitement de manière robuste, avec un système de flags et de polling automatique pour mettre à jour l'interface utilisateur en temps réel.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Backend (Node.js)
|
||||||
|
|
||||||
|
#### Fonctions principales
|
||||||
|
|
||||||
|
- **`createPendingFlag(folderHash, fileHash)`** : Crée un flag `.pending` avec métadonnées
|
||||||
|
- **`isFilePending(folderHash, fileHash)`** : Vérifie si un fichier est en cours de traitement
|
||||||
|
- **`cleanupOrphanedPendingFlags()`** : Nettoie les flags orphelins (> 1 heure)
|
||||||
|
|
||||||
|
#### Gestion des flags
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Structure d'un flag pending
|
||||||
|
{
|
||||||
|
fileHash: "abc123...",
|
||||||
|
folderHash: "def456...",
|
||||||
|
timestamp: "2025-09-16T02:58:29.606Z",
|
||||||
|
status: "processing"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### API Endpoints
|
||||||
|
|
||||||
|
- **`GET /api/folders/:folderHash/results`** : Retourne les résultats + informations pending
|
||||||
|
- **`POST /api/extract`** : Crée un flag pending avant traitement, retourne HTTP 202 si déjà en cours
|
||||||
|
|
||||||
|
#### Gestion d'erreur
|
||||||
|
|
||||||
|
- Suppression automatique des flags pending en cas d'erreur
|
||||||
|
- Gestion try/catch/finally pour garantir le nettoyage
|
||||||
|
- Nettoyage des flags orphelins au démarrage du serveur
|
||||||
|
|
||||||
|
### Frontend (React + Redux)
|
||||||
|
|
||||||
|
#### État Redux
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DocumentState {
|
||||||
|
// ... autres propriétés
|
||||||
|
pendingFiles: Array<{
|
||||||
|
fileHash: string
|
||||||
|
folderHash: string
|
||||||
|
timestamp: string
|
||||||
|
status: string
|
||||||
|
}>
|
||||||
|
hasPending: boolean
|
||||||
|
pollingInterval: NodeJS.Timeout | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Actions Redux
|
||||||
|
|
||||||
|
- **`setPendingFiles`** : Met à jour la liste des fichiers pending
|
||||||
|
- **`setPollingInterval`** : Gère l'intervalle de polling
|
||||||
|
- **`stopPolling`** : Arrête le polling et nettoie l'intervalle
|
||||||
|
|
||||||
|
#### Polling automatique
|
||||||
|
|
||||||
|
- Démarrage automatique si `hasPending = true`
|
||||||
|
- Polling toutes les 5 secondes
|
||||||
|
- Arrêt automatique quand plus de pending
|
||||||
|
- Nettoyage des intervalles au démontage des composants
|
||||||
|
|
||||||
|
## Flux de fonctionnement
|
||||||
|
|
||||||
|
### 1. Upload d'un fichier
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant F as Frontend
|
||||||
|
participant B as Backend
|
||||||
|
participant FS as FileSystem
|
||||||
|
|
||||||
|
F->>B: POST /api/extract (file + folderHash)
|
||||||
|
B->>B: Calculer fileHash
|
||||||
|
B->>B: Vérifier cache
|
||||||
|
B->>B: Vérifier pending
|
||||||
|
B->>FS: Créer flag .pending
|
||||||
|
B->>B: Traitement OCR/NER
|
||||||
|
B->>FS: Sauvegarder résultat .json
|
||||||
|
B->>FS: Supprimer flag .pending
|
||||||
|
B->>F: Retourner résultat
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Polling automatique
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant F as Frontend
|
||||||
|
participant B as Backend
|
||||||
|
|
||||||
|
F->>B: GET /api/folders/:hash/results
|
||||||
|
B->>F: { results: [], pending: [], hasPending: true }
|
||||||
|
F->>F: Démarrer polling (5s)
|
||||||
|
|
||||||
|
loop Polling
|
||||||
|
F->>B: GET /api/folders/:hash/results
|
||||||
|
B->>F: { results: [1], pending: [], hasPending: false }
|
||||||
|
F->>F: Arrêter polling
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Gestion d'erreur
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant B as Backend
|
||||||
|
participant FS as FileSystem
|
||||||
|
|
||||||
|
B->>FS: Créer flag .pending
|
||||||
|
B->>B: Traitement (ERREUR)
|
||||||
|
B->>FS: Supprimer flag .pending
|
||||||
|
B->>B: Retourner erreur 500
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Variables d'environnement
|
||||||
|
|
||||||
|
- **Polling interval** : 5000ms (5 secondes)
|
||||||
|
- **Cleanup threshold** : 1 heure pour les flags orphelins
|
||||||
|
|
||||||
|
### Structure des dossiers
|
||||||
|
|
||||||
|
```
|
||||||
|
uploads/
|
||||||
|
├── {folderHash}/
|
||||||
|
│ ├── {fileHash}.pdf
|
||||||
|
│ └── {fileHash}.jpg
|
||||||
|
cache/
|
||||||
|
├── {folderHash}/
|
||||||
|
│ ├── {fileHash}.json
|
||||||
|
│ └── {fileHash}.pending (temporaire)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avantages
|
||||||
|
|
||||||
|
1. **Robustesse** : Gestion des erreurs et nettoyage automatique
|
||||||
|
2. **Performance** : Évite les traitements en double
|
||||||
|
3. **UX** : Mise à jour automatique de l'interface
|
||||||
|
4. **Maintenance** : Nettoyage automatique des flags orphelins
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Tests manuels
|
||||||
|
|
||||||
|
1. **Upload simple** : Vérifier création/suppression du flag
|
||||||
|
2. **Upload en double** : Vérifier retour HTTP 202
|
||||||
|
3. **Erreur de traitement** : Vérifier suppression du flag
|
||||||
|
4. **Polling** : Vérifier mise à jour automatique
|
||||||
|
5. **Nettoyage** : Redémarrer serveur, vérifier nettoyage des orphelins
|
||||||
|
|
||||||
|
### Commandes de test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier l'état d'un dossier
|
||||||
|
curl -s http://localhost:3001/api/folders/{hash}/results | jq '.count, .hasPending'
|
||||||
|
|
||||||
|
# Tester l'upload
|
||||||
|
curl -X POST -F "document=@test.pdf" -F "folderHash={hash}" http://localhost:3001/api/extract
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Nettoyage manuel
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Supprimer tous les flags pending (attention !)
|
||||||
|
find cache/ -name "*.pending" -delete
|
||||||
|
|
||||||
|
# Vérifier les flags orphelins
|
||||||
|
find cache/ -name "*.pending" -mtime +0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
- Logs de création/suppression des flags
|
||||||
|
- Logs de polling dans la console frontend
|
||||||
|
- Métriques de temps de traitement
|
||||||
56
src/App.tsx
56
src/App.tsx
@ -1,17 +1,19 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useCallback } from 'react'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import { AppRouter } from './router'
|
import { AppRouter } from './router'
|
||||||
import { useAppDispatch, useAppSelector } from './store'
|
import { useAppDispatch, useAppSelector } from './store'
|
||||||
import {
|
import {
|
||||||
createDefaultFolderThunk,
|
createDefaultFolderThunk,
|
||||||
loadFolderResults,
|
loadFolderResults,
|
||||||
setCurrentFolderHash,
|
setCurrentFolderHash,
|
||||||
setBootstrapped
|
setBootstrapped,
|
||||||
|
setPollingInterval,
|
||||||
|
stopPolling
|
||||||
} from './store/documentSlice'
|
} from './store/documentSlice'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { documents, bootstrapped, currentFolderHash, folderResults } = useAppSelector((state) => state.document)
|
const { documents, bootstrapped, currentFolderHash, folderResults, hasPending, pollingInterval } = useAppSelector((state) => state.document)
|
||||||
|
|
||||||
// Bootstrap au démarrage de l'application avec système de dossiers
|
// Bootstrap au démarrage de l'application avec système de dossiers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -26,7 +28,7 @@ export default function App() {
|
|||||||
// Récupérer le hash du dossier depuis l'URL
|
// Récupérer le hash du dossier depuis l'URL
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const urlFolderHash = urlParams.get('hash')
|
const urlFolderHash = urlParams.get('hash')
|
||||||
|
|
||||||
console.log('🔍 [APP] Hash du dossier depuis URL:', urlFolderHash)
|
console.log('🔍 [APP] Hash du dossier depuis URL:', urlFolderHash)
|
||||||
|
|
||||||
const initializeFolder = async () => {
|
const initializeFolder = async () => {
|
||||||
@ -44,7 +46,7 @@ export default function App() {
|
|||||||
// Charger les résultats du dossier
|
// Charger les résultats du dossier
|
||||||
console.log('📁 [APP] Chargement des résultats du dossier:', folderHash)
|
console.log('📁 [APP] Chargement des résultats du dossier:', folderHash)
|
||||||
await dispatch(loadFolderResults(folderHash)).unwrap()
|
await dispatch(loadFolderResults(folderHash)).unwrap()
|
||||||
|
|
||||||
// Marquer le bootstrap comme terminé
|
// Marquer le bootstrap comme terminé
|
||||||
dispatch(setBootstrapped(true))
|
dispatch(setBootstrapped(true))
|
||||||
console.log('🎉 [APP] Bootstrap terminé avec le dossier:', folderHash)
|
console.log('🎉 [APP] Bootstrap terminé avec le dossier:', folderHash)
|
||||||
@ -54,7 +56,7 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ne pas refaire le bootstrap si déjà fait
|
// Ne pas refaire le bootstrap si déjà fait
|
||||||
if (bootstrapped && folderResults.length > 0) {
|
if (bootstrapped) {
|
||||||
console.log('⏭️ [APP] Bootstrap déjà effectué, dossier:', currentFolderHash)
|
console.log('⏭️ [APP] Bootstrap déjà effectué, dossier:', currentFolderHash)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -62,5 +64,41 @@ export default function App() {
|
|||||||
initializeFolder()
|
initializeFolder()
|
||||||
}, [dispatch, bootstrapped, currentFolderHash, folderResults.length])
|
}, [dispatch, bootstrapped, currentFolderHash, folderResults.length])
|
||||||
|
|
||||||
|
// Fonction pour démarrer le polling
|
||||||
|
const startPolling = useCallback((folderHash: string) => {
|
||||||
|
console.log('🔄 [APP] Démarrage du polling pour le dossier:', folderHash)
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
console.log('🔄 [APP] Polling - Vérification des résultats...')
|
||||||
|
dispatch(loadFolderResults(folderHash))
|
||||||
|
}, 5000) // Polling toutes les 5 secondes
|
||||||
|
|
||||||
|
dispatch(setPollingInterval(interval))
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
|
// Fonction pour arrêter le polling
|
||||||
|
const stopPollingCallback = useCallback(() => {
|
||||||
|
console.log('⏹️ [APP] Arrêt du polling')
|
||||||
|
dispatch(stopPolling())
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
|
// Gestion du polling basé sur l'état hasPending
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasPending && currentFolderHash && !pollingInterval) {
|
||||||
|
startPolling(currentFolderHash)
|
||||||
|
} else if (!hasPending && pollingInterval) {
|
||||||
|
stopPollingCallback()
|
||||||
|
}
|
||||||
|
}, [hasPending, currentFolderHash, pollingInterval, startPolling, stopPollingCallback])
|
||||||
|
|
||||||
|
// Nettoyage au démontage du composant
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (pollingInterval) {
|
||||||
|
clearInterval(pollingInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [pollingInterval])
|
||||||
|
|
||||||
return <AppRouter />
|
return <AppRouter />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,13 @@ export interface FolderResponse {
|
|||||||
success: boolean
|
success: boolean
|
||||||
folderHash: string
|
folderHash: string
|
||||||
results: FolderResult[]
|
results: FolderResult[]
|
||||||
|
pending: Array<{
|
||||||
|
fileHash: string
|
||||||
|
folderHash: string
|
||||||
|
timestamp: string
|
||||||
|
status: string
|
||||||
|
}>
|
||||||
|
hasPending: boolean
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,15 @@ interface DocumentState {
|
|||||||
currentFolderHash: string | null
|
currentFolderHash: string | null
|
||||||
folderResults: FolderResult[]
|
folderResults: FolderResult[]
|
||||||
currentResultIndex: number
|
currentResultIndex: number
|
||||||
|
// Propriétés pour le système de pending
|
||||||
|
pendingFiles: Array<{
|
||||||
|
fileHash: string
|
||||||
|
folderHash: string
|
||||||
|
timestamp: string
|
||||||
|
status: string
|
||||||
|
}>
|
||||||
|
hasPending: boolean
|
||||||
|
pollingInterval: NodeJS.Timeout | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction pour charger l'état depuis localStorage
|
// Fonction pour charger l'état depuis localStorage
|
||||||
@ -74,6 +83,10 @@ const initialState: DocumentState = {
|
|||||||
currentFolderHash: null,
|
currentFolderHash: null,
|
||||||
folderResults: [],
|
folderResults: [],
|
||||||
currentResultIndex: 0,
|
currentResultIndex: 0,
|
||||||
|
// Propriétés pour le système de pending
|
||||||
|
pendingFiles: [],
|
||||||
|
hasPending: false,
|
||||||
|
pollingInterval: null,
|
||||||
...loadStateFromStorage()
|
...loadStateFromStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,6 +268,27 @@ const documentSlice = createSlice({
|
|||||||
state.folderResults = []
|
state.folderResults = []
|
||||||
state.currentResultIndex = 0
|
state.currentResultIndex = 0
|
||||||
},
|
},
|
||||||
|
// Reducers pour le système de pending
|
||||||
|
setPendingFiles: (state, action: PayloadAction<Array<{
|
||||||
|
fileHash: string
|
||||||
|
folderHash: string
|
||||||
|
timestamp: string
|
||||||
|
status: string
|
||||||
|
}>>) => {
|
||||||
|
state.pendingFiles = action.payload
|
||||||
|
state.hasPending = action.payload.length > 0
|
||||||
|
},
|
||||||
|
setPollingInterval: (state, action: PayloadAction<NodeJS.Timeout | null>) => {
|
||||||
|
state.pollingInterval = action.payload
|
||||||
|
},
|
||||||
|
stopPolling: (state) => {
|
||||||
|
if (state.pollingInterval) {
|
||||||
|
clearInterval(state.pollingInterval)
|
||||||
|
state.pollingInterval = null
|
||||||
|
}
|
||||||
|
state.hasPending = false
|
||||||
|
state.pendingFiles = []
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder
|
builder
|
||||||
@ -339,19 +373,28 @@ const documentSlice = createSlice({
|
|||||||
state.folderResults = action.payload.results
|
state.folderResults = action.payload.results
|
||||||
state.currentFolderHash = action.payload.folderHash
|
state.currentFolderHash = action.payload.folderHash
|
||||||
state.loading = false
|
state.loading = false
|
||||||
|
|
||||||
|
// Gérer les fichiers pending
|
||||||
|
state.pendingFiles = action.payload.pending || []
|
||||||
|
state.hasPending = action.payload.hasPending || false
|
||||||
|
|
||||||
// Convertir les résultats en documents pour la compatibilité
|
// Convertir les résultats en documents pour la compatibilité
|
||||||
state.documents = action.payload.results.map((result, index) => ({
|
state.documents = action.payload.results.map((result, index) => ({
|
||||||
id: result.fileHash,
|
id: result.fileHash,
|
||||||
name: result.document.fileName,
|
name: result.document.fileName,
|
||||||
mimeType: result.document.mimeType,
|
mimeType: result.document.mimeType,
|
||||||
size: result.document.fileSize,
|
size: 0, // Taille non disponible dans la structure actuelle
|
||||||
uploadDate: new Date(result.document.uploadTimestamp),
|
uploadDate: new Date(result.document.uploadTimestamp),
|
||||||
status: 'completed' as const,
|
status: 'completed' as const,
|
||||||
previewUrl: `blob:folder-${result.fileHash}`
|
previewUrl: `blob:folder-${result.fileHash}`
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
console.log(`[STORE] Dossier chargé: ${action.payload.results.length} résultats, ${action.payload.pending?.length || 0} pending`)
|
||||||
|
console.log(`[STORE] Documents mappés:`, state.documents.map(d => ({ id: d.id, name: d.name, status: d.status })))
|
||||||
})
|
})
|
||||||
.addCase(loadFolderResults.pending, (state) => {
|
.addCase(loadFolderResults.pending, (state) => {
|
||||||
state.loading = true
|
// Ne pas afficher la barre de progression pour le chargement initial des résultats
|
||||||
|
// state.loading = true
|
||||||
})
|
})
|
||||||
.addCase(loadFolderResults.rejected, (state, action) => {
|
.addCase(loadFolderResults.rejected, (state, action) => {
|
||||||
state.loading = false
|
state.loading = false
|
||||||
@ -371,16 +414,19 @@ const documentSlice = createSlice({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
setCurrentDocument,
|
setCurrentDocument,
|
||||||
clearResults,
|
clearResults,
|
||||||
addDocuments,
|
addDocuments,
|
||||||
removeDocument,
|
removeDocument,
|
||||||
setOcrProgress,
|
setOcrProgress,
|
||||||
setLlmProgress,
|
setLlmProgress,
|
||||||
setBootstrapped,
|
setBootstrapped,
|
||||||
setCurrentFolderHash,
|
setCurrentFolderHash,
|
||||||
setCurrentResultIndex,
|
setCurrentResultIndex,
|
||||||
clearFolderResults
|
clearFolderResults,
|
||||||
|
setPendingFiles,
|
||||||
|
setPollingInterval,
|
||||||
|
stopPolling
|
||||||
} = documentSlice.actions
|
} = documentSlice.actions
|
||||||
export const documentReducer = documentSlice.reducer
|
export const documentReducer = documentSlice.reducer
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user