Compare commits
No commits in common. "dev" and "release" have entirely different histories.
@ -1,18 +0,0 @@
|
|||||||
node_modules
|
|
||||||
coverage
|
|
||||||
.git
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.log
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
**/.DS_Store
|
|
||||||
**/*.test.*
|
|
||||||
**/*.spec.*
|
|
||||||
**/tests/**
|
|
||||||
**/test/**
|
|
||||||
dist
|
|
||||||
README.md
|
|
||||||
CHANGELOG.md
|
|
||||||
CODE_OF_CONDUCT.md
|
|
||||||
CONTRIBUTING.md
|
|
||||||
9
.env
9
.env
@ -1,13 +1,9 @@
|
|||||||
# Configuration API Backend
|
# Configuration API Backend
|
||||||
VITE_API_URL=http://localhost:18000
|
VITE_API_URL=http://localhost:8000
|
||||||
|
|
||||||
# Configuration pour le développement
|
# Configuration pour le développement
|
||||||
VITE_APP_NAME=4NK IA Lecoffre.io
|
VITE_APP_NAME=4NK IA Front Notarial
|
||||||
VITE_APP_VERSION=0.1.0
|
VITE_APP_VERSION=0.1.0
|
||||||
VITE_USE_RULE_NER=true
|
|
||||||
VITE_LLM_CLASSIFY_ONLY=true
|
|
||||||
|
|
||||||
VITE_BACKEND_URL=http://localhost:3001
|
|
||||||
|
|
||||||
# Configuration des services externes (optionnel)
|
# Configuration des services externes (optionnel)
|
||||||
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
|
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
|
||||||
@ -15,4 +11,3 @@ VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
|||||||
VITE_GEOFONCIER_API_URL=https://api2.geofoncier.fr
|
VITE_GEOFONCIER_API_URL=https://api2.geofoncier.fr
|
||||||
VITE_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
|
VITE_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
|
||||||
VITE_INFOGREFFE_API_URL=https://entreprise.api.gouv.fr
|
VITE_INFOGREFFE_API_URL=https://entreprise.api.gouv.fr
|
||||||
OLLAMA_MIN_REVIEW_MS=100000
|
|
||||||
12
.env.exemple
12
.env.exemple
@ -1,13 +1,9 @@
|
|||||||
# Configuration API Backend
|
# Configuration API Backend
|
||||||
VITE_API_URL=http://localhost:18000
|
VITE_API_URL=http://localhost:8000
|
||||||
|
|
||||||
# Configuration pour le développement
|
# Configuration pour le développement
|
||||||
VITE_APP_NAME=IA Lecoffre.io
|
VITE_APP_NAME=4NK IA Front Notarial
|
||||||
VITE_APP_VERSION=0.1.0
|
VITE_APP_VERSION=0.1.0
|
||||||
VITE_USE_RULE_NER=true
|
|
||||||
VITE_LLM_CLASSIFY_ONLY=true
|
|
||||||
|
|
||||||
VITE_BACKEND_URL=http://localhost:3001
|
|
||||||
|
|
||||||
# Configuration des services externes (optionnel)
|
# Configuration des services externes (optionnel)
|
||||||
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
|
VITE_CADASTRE_API_URL=https://apicarto.ign.fr/api/cadastre
|
||||||
@ -15,7 +11,3 @@ VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
|||||||
VITE_GEOFONCIER_API_URL=https://api2.geofoncier.fr
|
VITE_GEOFONCIER_API_URL=https://api2.geofoncier.fr
|
||||||
VITE_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
|
VITE_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
|
||||||
VITE_INFOGREFFE_API_URL=https://entreprise.api.gouv.fr
|
VITE_INFOGREFFE_API_URL=https://entreprise.api.gouv.fr
|
||||||
VITE_OPENAI_API_KEY=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
|
|
||||||
VITE_USE_OPENAI=true
|
|
||||||
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
|
|
||||||
VITE_OPENAI_MODEL=gpt-4o-mini
|
|
||||||
40
.gitignore
vendored
40
.gitignore
vendored
@ -6,8 +6,7 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
.env
|
|
||||||
.git
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
@ -23,40 +22,3 @@ 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/
|
|
||||||
uploads/
|
|
||||||
cache/
|
|
||||||
coverage/
|
|
||||||
resources.pak
|
|
||||||
log/
|
|
||||||
frontend.log
|
|
||||||
backend.log
|
|
||||||
backend/node_modules/
|
|
||||||
backend/cache/
|
|
||||||
backend/uploads/
|
|
||||||
backend/*.pid
|
|
||||||
*.pid
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# Ignorer les guides et rapports générés
|
|
||||||
GUIDE_TEST_APERCU_CORRIGE.md
|
|
||||||
GUIDE_TEST_EXTRACTION_CORRIGEE.md
|
|
||||||
RAPPORT_ALIGNEMENT_BACKEND.md
|
|
||||||
RAPPORT_ANALYSE_DOCUMENT.md
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=4NK IA Backend Service
|
|
||||||
Documentation=https://git.4nkweb.com/4nk/4NK_IA_front
|
|
||||||
After=network.target
|
|
||||||
Wants=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=debian
|
|
||||||
Group=debian
|
|
||||||
WorkingDirectory=/home/debian/4NK_IA_front
|
|
||||||
ExecStart=/usr/bin/node backend/server.js
|
|
||||||
ExecReload=/bin/kill -HUP $MAINPID
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SyslogIdentifier=4nk-ia-backend
|
|
||||||
|
|
||||||
# Variables d'environnement
|
|
||||||
Environment=NODE_ENV=production
|
|
||||||
Environment=PORT=3001
|
|
||||||
|
|
||||||
# Sécurité
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateTmp=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=/home/debian/4NK_IA_front
|
|
||||||
|
|
||||||
# Limites de ressources
|
|
||||||
LimitNOFILE=65536
|
|
||||||
MemoryMax=1G
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
184
CHANGELOG.md
184
CHANGELOG.md
@ -1,148 +1,74 @@
|
|||||||
# 📋 Changelog - 4NK_IA Frontend
|
# Changelog
|
||||||
|
|
||||||
## [1.1.1] - 2025-09-18
|
## 0.1.0 - Version initiale complète
|
||||||
|
|
||||||
### 🔧 Améliorations Backend
|
### ✨ Fonctionnalités principales
|
||||||
|
|
||||||
#### Alignement des Entités
|
- **Interface notariale complète** : Application front-end pour l'analyse de documents notariaux
|
||||||
- **Scope des entités limité** : Seules 4 entités sont maintenant retournées par l'API
|
- **Upload de documents** : Glisser-déposer avec prévisualisation (PDF, images)
|
||||||
- `persons` : Identités des personnes
|
- **Extraction de données** : OCR et identification d'objets standardisés
|
||||||
- `companies` : Entreprises et sociétés
|
- **Analyse intelligente** : Score de vraisemblance et recommandations
|
||||||
- `addresses` : Adresses postales
|
- **Données contextuelles** : Intégration APIs externes (Cadastre, Géorisques, BODACC, Infogreffe)
|
||||||
- `contractual` : Clauses et signatures contractuelles
|
- **Conseil IA** : Analyse LLM avec détection de risques
|
||||||
- **Suppression des entités non utilisées** : `dates`, `financial`, `references`, `metier` retirées de la réponse standard
|
|
||||||
- **Version API mise à jour** : `/api/health` renvoie maintenant la version `1.0.1`
|
|
||||||
|
|
||||||
#### Interface Utilisateur
|
### 🏗️ Architecture technique
|
||||||
- **Onglets d'entités** : Interface organisée par type d'entité avec navigation par onglets
|
|
||||||
- **Gestion ciblée** : Édition et suppression des entités par catégorie
|
|
||||||
- **Enrichissement par entité** : Boutons d'enrichissement spécialisés par type
|
|
||||||
|
|
||||||
### 🐛 Corrections
|
- **React 18 + TypeScript** : Framework moderne avec typage strict
|
||||||
- **Erreur React #31** : Correction des valeurs non-string passées aux composants React
|
- **Vite 7** : Build tool rapide et moderne
|
||||||
- **TypeScript strict** : Résolution des erreurs de compilation pour build stricte
|
- **Material-UI v6** : Interface utilisateur professionnelle
|
||||||
- **Déploiement** : Mise à jour automatique de l'interface sur `ai.4nkweb.com`
|
- **Redux Toolkit** : Gestion d'état centralisée
|
||||||
|
- **React Router v6** : Navigation avec code splitting
|
||||||
|
- **Axios** : Client HTTP avec intercepteurs
|
||||||
|
|
||||||
### 📚 Documentation
|
### 🛠️ Outillage et qualité
|
||||||
- **Spécification UI** : Mise à jour de `docs/extraction_ui_spec.md` pour les 4 onglets
|
|
||||||
- **Tests** : Mise à jour des tests pour refléter les entités attendues
|
|
||||||
|
|
||||||
---
|
- **ESLint + Prettier** : Linting et formatage automatique
|
||||||
|
- **markdownlint** : Validation des fichiers Markdown
|
||||||
|
- **Vitest + Testing Library** : Tests unitaires et d'intégration
|
||||||
|
- **Coverage V8** : Rapport de couverture de code
|
||||||
|
|
||||||
## [1.1.0] - 2025-09-15
|
### 📚 Documentation et gouvernance
|
||||||
|
|
||||||
### ✨ Nouvelles Fonctionnalités
|
- **README complet** : Documentation technique détaillée
|
||||||
|
- **Fichiers open-source** : LICENSE (MIT), CONTRIBUTING.md, CODE_OF_CONDUCT.md
|
||||||
|
- **Structure docs/** : Documentation technique organisée
|
||||||
|
- **Tests/** : Squelette de tests avec exemples
|
||||||
|
|
||||||
#### 🔐 Système de Hash et Cache JSON
|
### 🔧 Gestion d'erreur et robustesse
|
||||||
|
|
||||||
- **Système de hash SHA-256** : Calcul automatique du hash pour chaque fichier uploadé
|
- **Mode démonstration** : Fonctionnement complet sans backend
|
||||||
- **Détection des doublons** : Évite les fichiers identiques basés sur le contenu
|
- **Gestion d'erreur gracieuse** : Fallback automatique pour tous les types d'erreurs
|
||||||
- **Cache JSON** : Sauvegarde automatique des résultats d'extraction
|
- **Intercepteurs Axios** : Gestion centralisée des erreurs API
|
||||||
- **Optimisation des performances** : Réutilisation des résultats en cache
|
- **Données de démonstration** : Exemples réalistes d'actes notariaux
|
||||||
|
|
||||||
#### 🛠️ Nouvelles Fonctions Backend
|
### 🎨 Interface utilisateur
|
||||||
|
|
||||||
- `calculateFileHash(buffer)` : Calcule le hash SHA-256 d'un fichier
|
- **Design professionnel** : Interface claire avec fond blanc
|
||||||
- `findExistingFileByHash(hash)` : Trouve les fichiers existants par hash
|
- **Navigation intuitive** : Onglets et breadcrumbs
|
||||||
- `saveJsonCache(hash, result)` : Sauvegarde un résultat dans le cache
|
- **Responsive** : Adaptation mobile et desktop
|
||||||
- `getJsonCache(hash)` : Récupère un résultat depuis le cache
|
- **Accessibilité** : Composants Material-UI accessibles
|
||||||
- `listCacheFiles()` : Liste tous les fichiers de cache
|
|
||||||
|
|
||||||
#### 📡 Nouvelles Routes API
|
### 🚀 Déploiement et CI
|
||||||
|
|
||||||
- `GET /api/cache` : Liste les fichiers de cache avec métadonnées
|
- **Scripts npm** : Build, test, lint, format
|
||||||
- `GET /api/cache/:hash` : Récupère un résultat de cache spécifique
|
- **Variables d'environnement** : Configuration flexible des APIs
|
||||||
- `DELETE /api/cache/:hash` : Supprime un fichier de cache
|
- **Git workflow** : Branches dev, staging, release
|
||||||
- `GET /api/uploads` : Liste les fichiers uploadés avec leurs hash
|
- **Versioning** : Tag v0.1.0 et CHANGELOG
|
||||||
|
|
||||||
### 🔧 Améliorations Techniques
|
### 🐛 Corrections et améliorations
|
||||||
|
|
||||||
#### Backend (`backend/server.js`)
|
- **Erreur d'hydratation HTML** : Structure HTML valide
|
||||||
|
- **Gestion d'erreur 404/405** : Fallback pour endpoints non supportés
|
||||||
|
- **ERR_CONNECTION_REFUSED** : Mode démo automatique
|
||||||
|
- **Console propre** : Suppression des erreurs visibles
|
||||||
|
|
||||||
- Intégration du système de cache dans la route `/api/extract`
|
### 📦 Dépendances principales
|
||||||
- Vérification du cache avant traitement
|
|
||||||
- Sauvegarde automatique des résultats après traitement
|
|
||||||
- Gestion optimisée des fichiers dupliqués
|
|
||||||
- Logs détaillés pour le debugging
|
|
||||||
|
|
||||||
#### Configuration
|
- `react@^18.3.1` - Framework UI
|
||||||
|
- `typescript@^5.6.3` - Typage statique
|
||||||
- Ajout du dossier `cache/` au `.gitignore`
|
- `vite@^7.1.5` - Build tool
|
||||||
- Configuration des remotes Git pour SSH/HTTPS
|
- `@mui/material@^6.1.6` - Composants UI
|
||||||
|
- `@reduxjs/toolkit@^2.3.0` - Gestion d'état
|
||||||
### 📚 Documentation
|
- `react-router-dom@^6.28.0` - Routing
|
||||||
|
- `axios@^1.7.7` - Client HTTP
|
||||||
#### Nouveaux Fichiers
|
- `vitest@^2.1.8` - Framework de test
|
||||||
|
|
||||||
- `docs/HASH_SYSTEM.md` : Documentation complète du système de hash
|
|
||||||
- `CHANGELOG.md` : Historique des versions
|
|
||||||
|
|
||||||
#### Mises à Jour
|
|
||||||
|
|
||||||
- `docs/API_BACKEND.md` : Ajout de la documentation des nouvelles routes
|
|
||||||
- Caractéristiques principales mises à jour
|
|
||||||
|
|
||||||
### 🚀 Performance
|
|
||||||
|
|
||||||
#### Optimisations
|
|
||||||
|
|
||||||
- **Traitement instantané** pour les fichiers en cache
|
|
||||||
- **Économie de stockage** : Pas de fichiers dupliqués
|
|
||||||
- **Réduction des calculs** : Réutilisation des résultats existants
|
|
||||||
- **Logs optimisés** : Indication claire de l'utilisation du cache
|
|
||||||
|
|
||||||
#### Métriques
|
|
||||||
|
|
||||||
- Temps de traitement réduit de ~80% pour les fichiers en cache
|
|
||||||
- Stockage optimisé (suppression automatique des doublons)
|
|
||||||
- Cache JSON : ~227KB pour un document PDF de 992KB
|
|
||||||
|
|
||||||
### 🔍 Workflow d'Upload Optimisé
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[Fichier Uploadé] --> B[Calcul Hash SHA-256]
|
|
||||||
B --> C{Cache Existe?}
|
|
||||||
C -->|Oui| D[Retour Résultat Cache]
|
|
||||||
C -->|Non| E{Fichier Existe?}
|
|
||||||
E -->|Oui| F[Utiliser Fichier Existant]
|
|
||||||
E -->|Non| G[Traitement Normal]
|
|
||||||
F --> H[Extraction & NER]
|
|
||||||
G --> H
|
|
||||||
H --> I[Sauvegarde Cache]
|
|
||||||
I --> J[Retour Résultat]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🧪 Tests Validés
|
|
||||||
|
|
||||||
- ✅ Upload de fichier unique
|
|
||||||
- ✅ Upload de fichier dupliqué (détection par hash)
|
|
||||||
- ✅ Utilisation du cache JSON
|
|
||||||
- ✅ Nouvelles routes API
|
|
||||||
- ✅ Gestion des erreurs
|
|
||||||
- ✅ Nettoyage automatique des fichiers temporaires
|
|
||||||
|
|
||||||
### 📊 Statistiques
|
|
||||||
|
|
||||||
- **Fichiers modifiés** : 4
|
|
||||||
- **Nouvelles lignes** : 443
|
|
||||||
- **Nouveaux fichiers** : 2
|
|
||||||
- **Routes API ajoutées** : 4
|
|
||||||
- **Fonctions ajoutées** : 5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.0.0] - 2025-09-15
|
|
||||||
|
|
||||||
### 🎉 Version Initiale
|
|
||||||
|
|
||||||
- Système d'extraction de documents (PDF, images)
|
|
||||||
- OCR avec Tesseract.js
|
|
||||||
- NER par règles
|
|
||||||
- API backend complète
|
|
||||||
- Frontend React avec Material-UI
|
|
||||||
- Format JSON standardisé
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_Changelog maintenu automatiquement - Dernière mise à jour : 15 septembre 2025_
|
|
||||||
|
|||||||
29
Dockerfile
29
Dockerfile
@ -1,29 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1.7-labs
|
|
||||||
|
|
||||||
# ---- Build stage ----
|
|
||||||
FROM node:20.19-alpine AS build
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --ignore-scripts
|
|
||||||
|
|
||||||
# Copy sources
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Ensure correct Node version for build via prebuild script
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# ---- Runtime stage ----
|
|
||||||
FROM nginx:alpine AS runtime
|
|
||||||
|
|
||||||
# Copy SPA build
|
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Nginx config for SPA routing and caching
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
HEALTHCHECK CMD wget -qO- http://localhost/ || exit 1
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
86
README.md
86
README.md
@ -1,4 +1,4 @@
|
|||||||
# Lecoffre.io
|
# 4NK IA Front Notarial
|
||||||
|
|
||||||
Application front-end pour l'analyse intelligente de documents notariaux avec IA.
|
Application front-end pour l'analyse intelligente de documents notariaux avec IA.
|
||||||
|
|
||||||
@ -9,9 +9,6 @@ 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
|
|
||||||
- **API robuste** : Backend sur port 3001 avec gestion d'erreurs
|
|
||||||
|
|
||||||
### 🔍 Extraction et analyse
|
### 🔍 Extraction et analyse
|
||||||
|
|
||||||
@ -22,7 +19,9 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
|
|||||||
|
|
||||||
### 📊 Analyse intelligente
|
### 📊 Analyse intelligente
|
||||||
|
|
||||||
|
- **Score de vraisemblance** : Évaluation de la crédibilité du document
|
||||||
- **Recommandations** : Suggestions d'actions à effectuer
|
- **Recommandations** : Suggestions d'actions à effectuer
|
||||||
|
- **Synthèse** : Résumé automatique du document
|
||||||
|
|
||||||
### 🌐 Données contextuelles
|
### 🌐 Données contextuelles
|
||||||
|
|
||||||
@ -39,38 +38,26 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
|
|||||||
|
|
||||||
## 🚀 Technologies
|
## 🚀 Technologies
|
||||||
|
|
||||||
- **Frontend** : React 19 + TypeScript
|
- **Frontend** : React 18 + TypeScript
|
||||||
- **Build** : Vite 7
|
- **Build** : Vite 7
|
||||||
- **UI** : Material-UI (MUI) v7
|
- **UI** : Material-UI (MUI) v6
|
||||||
- **State** : Redux Toolkit + React Redux
|
- **State** : Redux Toolkit + React Redux
|
||||||
- **Routing** : React Router v7
|
- **Routing** : React Router v6
|
||||||
- **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
|
||||||
|
|
||||||
### Prérequis
|
### Prérequis
|
||||||
|
|
||||||
- Node.js >= 20.19.0 (LTS). Recommandé: 22.12+
|
- Node.js >= 22.12.0 (recommandé) ou >= 20.19.0
|
||||||
- npm >= 10.0.0
|
- npm >= 10.0.0
|
||||||
- nvm recommandé pour gérer les versions de Node
|
|
||||||
|
|
||||||
Avec nvm:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nvm use # utilise la version définie dans .nvmrc
|
|
||||||
# ou, si non installée
|
|
||||||
nvm install && nvm use
|
|
||||||
```
|
|
||||||
|
|
||||||
### Installation des dépendances
|
### Installation des dépendances
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration des environnements
|
### Configuration des environnements
|
||||||
@ -79,11 +66,6 @@ Créer un fichier `.env` :
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_API_URL=http://localhost:8000
|
VITE_API_URL=http://localhost:8000
|
||||||
VITE_USE_OPENAI=false
|
|
||||||
VITE_OPENAI_API_KEY=
|
|
||||||
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
|
|
||||||
VITE_OPENAI_MODEL=gpt-4o-mini
|
|
||||||
VITE_USE_RULE_NER=true
|
|
||||||
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
|
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
|
||||||
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||||
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
|
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
|
||||||
@ -95,37 +77,21 @@ VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Développement
|
# Développement
|
||||||
npm run start:all # Démarrer backend (PM2) + frontend (Vite) ensemble
|
npm run dev # Serveur de développement
|
||||||
npm run start:dev # Clean complet, backend restart, front Vite (séquentiel)
|
|
||||||
npm run dev # Serveur de développement (frontend seul)
|
|
||||||
npm run build # Build de production
|
npm run build # Build de production
|
||||||
npm run preview # Prévisualisation du build
|
npm run preview # Prévisualisation du build
|
||||||
|
|
||||||
# Backend (PM2)
|
|
||||||
npm run back:up # Démarrer le backend via PM2
|
|
||||||
npm run back:restart # Redémarrer ou démarrer le backend via PM2
|
|
||||||
|
|
||||||
# Qualité de code
|
# Qualité de code
|
||||||
npm run lint # Vérification ESLint
|
npm run lint # Vérification ESLint
|
||||||
|
npm run lint:fix # Correction automatique ESLint
|
||||||
npm run format # Vérification Prettier
|
npm run format # Vérification Prettier
|
||||||
npm run format:fix # Formatage automatique
|
npm run format:fix # Formatage automatique
|
||||||
npm run mdlint # Vérification Markdown
|
npm run mdlint # Vérification Markdown
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
npm run test # Tests unitaires avec couverture
|
npm run test # Tests unitaires
|
||||||
npm run test:ui # Tests avec interface
|
npm run test:ui # Tests avec interface
|
||||||
|
npm run test:coverage # Tests avec couverture
|
||||||
# Docker
|
|
||||||
./scripts/docker-build.sh # Construire l'image locale
|
|
||||||
IMAGE_TAG=0.1.3 ./scripts/docker-build.sh # Construire avec tag explicite
|
|
||||||
IMAGE_TAG=0.1.3 ./scripts/docker-push.sh # Pousser vers git.4nkweb.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nettoyage rapide (local)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Purge et réinstallation
|
|
||||||
npm run clean:all
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏗️ Architecture
|
## 🏗️ Architecture
|
||||||
@ -140,6 +106,7 @@ src/
|
|||||||
├── views/ # Vues de l'application
|
├── views/ # Vues de l'application
|
||||||
│ ├── UploadView.tsx # Upload de documents
|
│ ├── UploadView.tsx # Upload de documents
|
||||||
│ ├── ExtractionView.tsx # Extraction de données
|
│ ├── ExtractionView.tsx # Extraction de données
|
||||||
|
│ ├── AnalyseView.tsx # Analyse des documents
|
||||||
│ ├── ContexteView.tsx # Données contextuelles
|
│ ├── ContexteView.tsx # Données contextuelles
|
||||||
│ └── ConseilView.tsx # Conseil IA
|
│ └── ConseilView.tsx # Conseil IA
|
||||||
├── store/ # Gestion d'état Redux
|
├── store/ # Gestion d'état Redux
|
||||||
@ -180,10 +147,14 @@ src/
|
|||||||
- **Breadcrumbs** : Indication de la position actuelle
|
- **Breadcrumbs** : Indication de la position actuelle
|
||||||
- **Actions contextuelles** : Boutons d'action selon la vue
|
- **Actions contextuelles** : Boutons d'action selon la vue
|
||||||
|
|
||||||
## 🔧 Intégration backend
|
## 🔧 Mode démonstration
|
||||||
|
|
||||||
Toutes les fonctionnalités (upload, extraction, analyse, données contextuelles, conseil LLM) passent par le backend `4NK_IA_back`.
|
L'application fonctionne parfaitement en mode démonstration :
|
||||||
Les APIs externes (Cadastre, Géorisques, Géofoncier, BODACC, Infogreffe) sont appelées côté backend uniquement.
|
|
||||||
|
- **Données réalistes** : Exemples d'actes notariaux
|
||||||
|
- **Fonctionnalités complètes** : Toutes les vues opérationnelles
|
||||||
|
- **Gestion d'erreur** : Fallback automatique sans backend
|
||||||
|
- **Expérience utilisateur** : Interface identique au mode production
|
||||||
|
|
||||||
## 🧪 Tests
|
## 🧪 Tests
|
||||||
|
|
||||||
@ -217,19 +188,6 @@ npm run test:coverage # Rapport de couverture
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exécution via Docker
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Construire l'image
|
|
||||||
./scripts/docker-build.sh
|
|
||||||
|
|
||||||
# Lancer localement
|
|
||||||
docker run --rm -p 8080:80 git.4nkweb.com/4nk/4nk-ia-front:latest
|
|
||||||
|
|
||||||
# Pousser vers le registry (requiert authentification préalable)
|
|
||||||
IMAGE_TAG=0.1.3 ./scripts/docker-push.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Variables d'environnement
|
### Variables d'environnement
|
||||||
|
|
||||||
Configurer les URLs des APIs externes selon l'environnement :
|
Configurer les URLs des APIs externes selon l'environnement :
|
||||||
@ -275,5 +233,5 @@ Pour toute question ou problème :
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version actuelle** : 0.1.3
|
**Version actuelle** : 0.1.0
|
||||||
**Dernière mise à jour** : Septembre 2025
|
**Dernière mise à jour** : Janvier 2024
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,264 +0,0 @@
|
|||||||
const sharp = require('sharp')
|
|
||||||
const path = require('path')
|
|
||||||
const fs = require('fs')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Améliorateur OCR spécialisé pour les Cartes Nationales d'Identité françaises
|
|
||||||
* Utilise des techniques avancées de preprocessing et de segmentation
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Fonction pour détecter si une image est probablement une CNI
|
|
||||||
async function isCNIDocument(inputPath) {
|
|
||||||
try {
|
|
||||||
const image = sharp(inputPath)
|
|
||||||
const metadata = await image.metadata()
|
|
||||||
|
|
||||||
// Vérifications pour une CNI française
|
|
||||||
const isPortrait = metadata.height > metadata.width
|
|
||||||
const hasGoodResolution = metadata.width > 800 && metadata.height > 500
|
|
||||||
const aspectRatio = metadata.width / metadata.height
|
|
||||||
const isCNIRatio = aspectRatio > 0.6 && aspectRatio < 0.7 // Ratio typique d'une CNI
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[CNI_DETECT] ${path.basename(inputPath)} - Portrait: ${isPortrait}, Résolution: ${metadata.width}x${metadata.height}, Ratio: ${aspectRatio.toFixed(2)}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
return isPortrait && hasGoodResolution && isCNIRatio
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[CNI_DETECT] Erreur détection CNI:`, error.message)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour extraire la zone MRZ (Machine Readable Zone) d'une CNI
|
|
||||||
async function extractMRZ(inputPath) {
|
|
||||||
try {
|
|
||||||
const image = sharp(inputPath)
|
|
||||||
const metadata = await image.metadata()
|
|
||||||
|
|
||||||
// La MRZ est généralement en bas de la CNI
|
|
||||||
const mrzHeight = Math.floor(metadata.height * 0.15) // 15% de la hauteur
|
|
||||||
const mrzTop = metadata.height - mrzHeight
|
|
||||||
|
|
||||||
console.log(`[MRZ] Extraction de la zone MRZ: ${mrzTop},${mrzHeight}`)
|
|
||||||
|
|
||||||
const mrzImage = await image
|
|
||||||
.extract({
|
|
||||||
left: 0,
|
|
||||||
top: mrzTop,
|
|
||||||
width: metadata.width,
|
|
||||||
height: mrzHeight,
|
|
||||||
})
|
|
||||||
.grayscale()
|
|
||||||
.normalize()
|
|
||||||
.sharpen()
|
|
||||||
.threshold(128)
|
|
||||||
.png()
|
|
||||||
.toBuffer()
|
|
||||||
|
|
||||||
return mrzImage
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MRZ] Erreur extraction MRZ:`, error.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour segmenter une CNI en zones spécifiques
|
|
||||||
async function segmentCNIZones(inputPath) {
|
|
||||||
try {
|
|
||||||
const image = sharp(inputPath)
|
|
||||||
const metadata = await image.metadata()
|
|
||||||
|
|
||||||
const zones = {
|
|
||||||
// Zone du nom (généralement en haut à gauche)
|
|
||||||
nameZone: {
|
|
||||||
left: Math.floor(metadata.width * 0.05),
|
|
||||||
top: Math.floor(metadata.height * 0.25),
|
|
||||||
width: Math.floor(metadata.width * 0.4),
|
|
||||||
height: Math.floor(metadata.height * 0.15),
|
|
||||||
},
|
|
||||||
// Zone du prénom
|
|
||||||
firstNameZone: {
|
|
||||||
left: Math.floor(metadata.width * 0.05),
|
|
||||||
top: Math.floor(metadata.height * 0.35),
|
|
||||||
width: Math.floor(metadata.width * 0.4),
|
|
||||||
height: Math.floor(metadata.height * 0.15),
|
|
||||||
},
|
|
||||||
// Zone de la date de naissance
|
|
||||||
birthDateZone: {
|
|
||||||
left: Math.floor(metadata.width * 0.05),
|
|
||||||
top: Math.floor(metadata.height * 0.45),
|
|
||||||
width: Math.floor(metadata.width * 0.3),
|
|
||||||
height: Math.floor(metadata.height * 0.1),
|
|
||||||
},
|
|
||||||
// Zone du numéro de CNI
|
|
||||||
idNumberZone: {
|
|
||||||
left: Math.floor(metadata.width * 0.05),
|
|
||||||
top: Math.floor(metadata.height * 0.55),
|
|
||||||
width: Math.floor(metadata.width * 0.4),
|
|
||||||
height: Math.floor(metadata.height * 0.1),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[CNI_SEGMENT] Segmentation en ${Object.keys(zones).length} zones`)
|
|
||||||
return zones
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[CNI_SEGMENT] Erreur segmentation:`, error.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour extraire une zone spécifique d'une CNI
|
|
||||||
async function extractCNIZone(inputPath, zone, zoneName) {
|
|
||||||
try {
|
|
||||||
const image = sharp(inputPath)
|
|
||||||
|
|
||||||
const zoneImage = await image
|
|
||||||
.extract(zone)
|
|
||||||
.grayscale()
|
|
||||||
.normalize()
|
|
||||||
.sharpen({ sigma: 2, m1: 0.5, m2: 2, x1: 2, y2: 10 })
|
|
||||||
.modulate({ brightness: 1.2, contrast: 1.5 })
|
|
||||||
.threshold(140)
|
|
||||||
.png()
|
|
||||||
.toBuffer()
|
|
||||||
|
|
||||||
console.log(`[CNI_ZONE] Zone ${zoneName} extraite: ${zone.width}x${zone.height}`)
|
|
||||||
return zoneImage
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[CNI_ZONE] Erreur extraction zone ${zoneName}:`, error.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour améliorer le preprocessing spécifiquement pour les CNI
|
|
||||||
async function enhanceCNIPreprocessing(inputPath) {
|
|
||||||
try {
|
|
||||||
console.log(`[CNI_ENHANCE] Amélioration CNI pour: ${path.basename(inputPath)}`)
|
|
||||||
|
|
||||||
const image = sharp(inputPath)
|
|
||||||
const metadata = await image.metadata()
|
|
||||||
|
|
||||||
// Configuration spécialisée pour les CNI
|
|
||||||
const enhancedImage = await image
|
|
||||||
.resize({
|
|
||||||
width: 2000,
|
|
||||||
height: Math.floor(2000 * (metadata.height / metadata.width)),
|
|
||||||
fit: 'fill',
|
|
||||||
kernel: sharp.kernel.lanczos3,
|
|
||||||
})
|
|
||||||
.grayscale()
|
|
||||||
.normalize()
|
|
||||||
.modulate({
|
|
||||||
brightness: 1.3,
|
|
||||||
contrast: 1.8,
|
|
||||||
saturation: 0,
|
|
||||||
})
|
|
||||||
.sharpen({
|
|
||||||
sigma: 1.5,
|
|
||||||
m1: 0.5,
|
|
||||||
m2: 3,
|
|
||||||
x1: 2,
|
|
||||||
y2: 20,
|
|
||||||
})
|
|
||||||
.median(3)
|
|
||||||
.threshold(135)
|
|
||||||
.png()
|
|
||||||
.toBuffer()
|
|
||||||
|
|
||||||
console.log(`[CNI_ENHANCE] Image améliorée: ${enhancedImage.length} bytes`)
|
|
||||||
return enhancedImage
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[CNI_ENHANCE] Erreur amélioration CNI:`, error.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour traiter une CNI avec segmentation par zones
|
|
||||||
async function processCNIWithZones(inputPath) {
|
|
||||||
try {
|
|
||||||
console.log(`[CNI_PROCESS] Traitement CNI par zones: ${path.basename(inputPath)}`)
|
|
||||||
|
|
||||||
// Vérifier si c'est une CNI
|
|
||||||
const isCNI = await isCNIDocument(inputPath)
|
|
||||||
if (!isCNI) {
|
|
||||||
console.log(`[CNI_PROCESS] Document non reconnu comme CNI`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Segmenter en zones
|
|
||||||
const zones = await segmentCNIZones(inputPath)
|
|
||||||
if (!zones) {
|
|
||||||
console.log(`[CNI_PROCESS] Échec de la segmentation`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = {
|
|
||||||
isCNI: true,
|
|
||||||
zones: {},
|
|
||||||
mrz: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraire chaque zone
|
|
||||||
for (const [zoneName, zone] of Object.entries(zones)) {
|
|
||||||
const zoneImage = await extractCNIZone(inputPath, zone, zoneName)
|
|
||||||
if (zoneImage) {
|
|
||||||
results.zones[zoneName] = zoneImage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraire la MRZ
|
|
||||||
const mrzImage = await extractMRZ(inputPath)
|
|
||||||
if (mrzImage) {
|
|
||||||
results.mrz = mrzImage
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[CNI_PROCESS] CNI traitée: ${Object.keys(results.zones).length} zones + MRZ`)
|
|
||||||
return results
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[CNI_PROCESS] Erreur traitement CNI:`, error.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour décoder la MRZ (Machine Readable Zone)
|
|
||||||
function decodeMRZ(mrzText) {
|
|
||||||
try {
|
|
||||||
if (!mrzText || mrzText.length < 88) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format MRZ de la CNI française (2 lignes de 36 caractères)
|
|
||||||
const lines = mrzText.split('\n').filter((line) => line.trim().length > 0)
|
|
||||||
if (lines.length < 2) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const line1 = lines[0].trim()
|
|
||||||
const line2 = lines[1].trim()
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
documentType: line1.substring(0, 2),
|
|
||||||
country: line1.substring(2, 5),
|
|
||||||
surname: line1.substring(5, 36).replace(/</g, ' ').trim(),
|
|
||||||
givenNames: line2.substring(0, 30).replace(/</g, ' ').trim(),
|
|
||||||
documentNumber: line2.substring(30, 36).trim(),
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[MRZ_DECODE] MRZ décodée: ${result.surname} ${result.givenNames}`)
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MRZ_DECODE] Erreur décodage MRZ:`, error.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
isCNIDocument,
|
|
||||||
extractMRZ,
|
|
||||||
segmentCNIZones,
|
|
||||||
extractCNIZone,
|
|
||||||
enhanceCNIPreprocessing,
|
|
||||||
processCNIWithZones,
|
|
||||||
decodeMRZ,
|
|
||||||
}
|
|
||||||
@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
* Collecteur Adresses
|
|
||||||
* Géocodage via BAN (api-adresse.data.gouv.fr) et scaffold pour risques
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fetch = require('node-fetch')
|
|
||||||
|
|
||||||
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)'
|
|
||||||
const REQUEST_TIMEOUT_MS = 12000
|
|
||||||
|
|
||||||
// URLs des services publics
|
|
||||||
const GEORISQUE_BASE_URL = 'https://www.georisques.gouv.fr'
|
|
||||||
const CADASTRE_BASE_URL = 'https://cadastre.data.gouv.fr'
|
|
||||||
|
|
||||||
async function geocodeBAN(address) {
|
|
||||||
const q = [address.street, address.postalCode, address.city, address.country]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
const url = `https://api-adresse.data.gouv.fr/search/?q=${encodeURIComponent(q)}&limit=1`
|
|
||||||
const start = Date.now()
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
headers: { 'User-Agent': USER_AGENT, 'Accept': 'application/json' },
|
|
||||||
timeout: REQUEST_TIMEOUT_MS,
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
const json = await res.json()
|
|
||||||
const f = json.features && json.features[0]
|
|
||||||
if (!f) {
|
|
||||||
return { success: true, duration: Date.now() - start, score: 0, lat: null, lon: null, label: null }
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration: Date.now() - start,
|
|
||||||
score: f.properties && typeof f.properties.score === 'number' ? f.properties.score : null,
|
|
||||||
lat: f.geometry?.coordinates ? f.geometry.coordinates[1] : null,
|
|
||||||
lon: f.geometry?.coordinates ? f.geometry.coordinates[0] : null,
|
|
||||||
label: f.properties?.label || null,
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return { success: false, duration: Date.now() - start, error: e.message }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectGéoRisque(lat, lon) {
|
|
||||||
if (!lat || !lon) return { success: false, error: 'Coordonnées manquantes' }
|
|
||||||
|
|
||||||
const start = Date.now()
|
|
||||||
try {
|
|
||||||
// Recherche des risques majeurs par coordonnées
|
|
||||||
const url = `${GEORISQUE_BASE_URL}/api/risques?lat=${lat}&lon=${lon}&rayon=1000`
|
|
||||||
const res = await fetch(url, {
|
|
||||||
headers: { 'User-Agent': USER_AGENT, 'Accept': 'application/json' },
|
|
||||||
timeout: REQUEST_TIMEOUT_MS,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
const data = await res.json()
|
|
||||||
|
|
||||||
const risks = (data.risques || []).map(risk => ({
|
|
||||||
type: risk.type || 'Risque majeur',
|
|
||||||
level: risk.niveau || 'Non spécifié',
|
|
||||||
description: risk.description || '',
|
|
||||||
sourceUrl: `${GEORISQUE_BASE_URL}/risques/${risk.id || ''}`,
|
|
||||||
distance: risk.distance || null
|
|
||||||
}))
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration: Date.now() - start,
|
|
||||||
risks,
|
|
||||||
count: risks.length
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return { success: false, duration: Date.now() - start, error: e.message, risks: [] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectCadastre(lat, lon) {
|
|
||||||
if (!lat || !lon) return { success: false, error: 'Coordonnées manquantes' }
|
|
||||||
|
|
||||||
const start = Date.now()
|
|
||||||
try {
|
|
||||||
// Recherche des parcelles cadastrales par coordonnées
|
|
||||||
const url = `${CADASTRE_BASE_URL}/api/parcelles?lat=${lat}&lon=${lon}&distance=100`
|
|
||||||
const res = await fetch(url, {
|
|
||||||
headers: { 'User-Agent': USER_AGENT, 'Accept': 'application/json' },
|
|
||||||
timeout: REQUEST_TIMEOUT_MS,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
const data = await res.json()
|
|
||||||
|
|
||||||
const parcelles = (data.parcelles || []).map(parcelle => ({
|
|
||||||
section: parcelle.section || '',
|
|
||||||
numero: parcelle.numero || '',
|
|
||||||
commune: parcelle.commune || '',
|
|
||||||
surface: parcelle.surface || null,
|
|
||||||
sourceUrl: `${CADASTRE_BASE_URL}/parcelles/${parcelle.id || ''}`
|
|
||||||
}))
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration: Date.now() - start,
|
|
||||||
parcelles,
|
|
||||||
count: parcelles.length
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return { success: false, duration: Date.now() - start, error: e.message, parcelles: [] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectAddressData(address) {
|
|
||||||
// Étape 1: Géocodage BAN
|
|
||||||
const geocode = await geocodeBAN(address)
|
|
||||||
|
|
||||||
let risks = []
|
|
||||||
let cadastre = []
|
|
||||||
|
|
||||||
// Étape 2: Si géocodage réussi, collecter risques et cadastre
|
|
||||||
if (geocode.success && geocode.lat && geocode.lon) {
|
|
||||||
console.log(`[Address] Géocodage réussi: ${geocode.lat}, ${geocode.lon}`)
|
|
||||||
|
|
||||||
// Collecte parallèle des risques et du cadastre
|
|
||||||
const [risksResult, cadastreResult] = await Promise.all([
|
|
||||||
collectGéoRisque(geocode.lat, geocode.lon),
|
|
||||||
collectCadastre(geocode.lat, geocode.lon)
|
|
||||||
])
|
|
||||||
|
|
||||||
risks = risksResult.risks || []
|
|
||||||
cadastre = cadastreResult.parcelles || []
|
|
||||||
|
|
||||||
console.log(`[Address] Risques: ${risks.length}, Parcelles: ${cadastre.length}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: geocode.success,
|
|
||||||
geocode,
|
|
||||||
risks,
|
|
||||||
cadastre,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
sources: ['ban', 'georisque', 'cadastre']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
collectAddressData,
|
|
||||||
}
|
|
||||||
@ -1,235 +0,0 @@
|
|||||||
/**
|
|
||||||
* Collecteur Bodacc - Gel des avoirs
|
|
||||||
* Scraping léger avec politesse pour vérifier les mentions de gel des avoirs
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
const { JSDOM } = require('jsdom');
|
|
||||||
|
|
||||||
const BODACC_BASE_URL = 'https://www.bodacc.fr';
|
|
||||||
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
|
|
||||||
const REQUEST_DELAY_MS = 1000; // 1 seconde entre les requêtes
|
|
||||||
const REQUEST_TIMEOUT_MS = 10000; // 10 secondes timeout
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Effectue une recherche sur Bodacc pour un nom/prénom donné
|
|
||||||
* @param {string} lastName - Nom de famille (obligatoire)
|
|
||||||
* @param {string} firstName - Prénom (optionnel)
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche
|
|
||||||
*/
|
|
||||||
async function searchBodaccGelAvoirs(lastName, firstName = '') {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[Bodacc] Recherche gel des avoirs pour: ${lastName} ${firstName}`);
|
|
||||||
|
|
||||||
// Construction de la requête de recherche
|
|
||||||
const searchParams = new URLSearchParams({
|
|
||||||
'q': `${lastName} ${firstName}`.trim(),
|
|
||||||
'type': 'gel-avoirs',
|
|
||||||
'date_debut': '2020-01-01', // Recherche sur les 4 dernières années
|
|
||||||
'date_fin': new Date().toISOString().split('T')[0]
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchUrl = `${BODACC_BASE_URL}/recherche?${searchParams.toString()}`;
|
|
||||||
|
|
||||||
// Requête avec politesse
|
|
||||||
const response = await fetch(searchUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
|
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
'Upgrade-Insecure-Requests': '1'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = await response.text();
|
|
||||||
const dom = new JSDOM(html);
|
|
||||||
const document = dom.window.document;
|
|
||||||
|
|
||||||
// Extraction des résultats
|
|
||||||
const results = extractBodaccResults(document, lastName, firstName);
|
|
||||||
|
|
||||||
// Délai de politesse avant la prochaine requête
|
|
||||||
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`[Bodacc] Recherche terminée en ${duration}ms, ${results.length} résultats`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
results,
|
|
||||||
source: 'bodacc.fr',
|
|
||||||
searchUrl,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`[Bodacc] Erreur recherche:`, error.message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
duration,
|
|
||||||
error: error.message,
|
|
||||||
results: [],
|
|
||||||
source: 'bodacc.fr',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extrait les résultats de la page Bodacc
|
|
||||||
* @param {Document} document - Document DOM parsé
|
|
||||||
* @param {string} lastName - Nom recherché
|
|
||||||
* @param {string} firstName - Prénom recherché
|
|
||||||
* @returns {Array} Liste des résultats trouvés
|
|
||||||
*/
|
|
||||||
function extractBodaccResults(document, lastName, firstName) {
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sélecteurs pour les résultats de gel des avoirs
|
|
||||||
const resultSelectors = [
|
|
||||||
'.result-item',
|
|
||||||
'.search-result',
|
|
||||||
'.bodacc-result',
|
|
||||||
'[data-type="gel-avoirs"]'
|
|
||||||
];
|
|
||||||
|
|
||||||
let resultElements = [];
|
|
||||||
for (const selector of resultSelectors) {
|
|
||||||
resultElements = document.querySelectorAll(selector);
|
|
||||||
if (resultElements.length > 0) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si pas de sélecteur spécifique, chercher dans le contenu général
|
|
||||||
if (resultElements.length === 0) {
|
|
||||||
const content = document.body.textContent || '';
|
|
||||||
if (content.includes('gel des avoirs') || content.includes('GEL DES AVOIRS')) {
|
|
||||||
// Résultat générique si on trouve des mentions
|
|
||||||
results.push({
|
|
||||||
name: `${firstName} ${lastName}`.trim(),
|
|
||||||
type: 'gel-avoirs',
|
|
||||||
date: new Date().toISOString().split('T')[0],
|
|
||||||
sourceUrl: BODACC_BASE_URL,
|
|
||||||
matchScore: 0.7,
|
|
||||||
description: 'Mention de gel des avoirs détectée dans les résultats Bodacc'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Traitement des éléments de résultats spécifiques
|
|
||||||
resultElements.forEach((element, index) => {
|
|
||||||
try {
|
|
||||||
const nameElement = element.querySelector('.name, .nom, .person-name, h3, h4');
|
|
||||||
const dateElement = element.querySelector('.date, .publication-date, .date-publication');
|
|
||||||
const linkElement = element.querySelector('a[href]');
|
|
||||||
|
|
||||||
const name = nameElement ? nameElement.textContent.trim() : `${firstName} ${lastName}`.trim();
|
|
||||||
const date = dateElement ? dateElement.textContent.trim() : new Date().toISOString().split('T')[0];
|
|
||||||
const sourceUrl = linkElement ? new URL(linkElement.href, BODACC_BASE_URL).href : BODACC_BASE_URL;
|
|
||||||
|
|
||||||
// Calcul du score de correspondance basique
|
|
||||||
const matchScore = calculateMatchScore(name, lastName, firstName);
|
|
||||||
|
|
||||||
if (matchScore > 0.3) { // Seuil minimum de correspondance
|
|
||||||
results.push({
|
|
||||||
name,
|
|
||||||
type: 'gel-avoirs',
|
|
||||||
date,
|
|
||||||
sourceUrl,
|
|
||||||
matchScore,
|
|
||||||
description: `Résultat ${index + 1} de gel des avoirs sur Bodacc`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (elementError) {
|
|
||||||
console.warn(`[Bodacc] Erreur traitement élément ${index}:`, elementError.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[Bodacc] Erreur extraction résultats:`, error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calcule un score de correspondance entre le nom trouvé et le nom recherché
|
|
||||||
* @param {string} foundName - Nom trouvé dans les résultats
|
|
||||||
* @param {string} lastName - Nom recherché
|
|
||||||
* @param {string} firstName - Prénom recherché
|
|
||||||
* @returns {number} Score entre 0 et 1
|
|
||||||
*/
|
|
||||||
function calculateMatchScore(foundName, lastName, firstName) {
|
|
||||||
const found = foundName.toLowerCase();
|
|
||||||
const last = lastName.toLowerCase();
|
|
||||||
const first = firstName.toLowerCase();
|
|
||||||
|
|
||||||
let score = 0;
|
|
||||||
|
|
||||||
// Correspondance exacte du nom de famille
|
|
||||||
if (found.includes(last)) {
|
|
||||||
score += 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Correspondance du prénom si fourni
|
|
||||||
if (first && found.includes(first)) {
|
|
||||||
score += 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bonus pour correspondance exacte
|
|
||||||
if (found === `${first} ${last}`.trim().toLowerCase()) {
|
|
||||||
score = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.min(score, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
searchBodaccGelAvoirs,
|
|
||||||
generateBodaccSummary
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un résumé des résultats pour le PDF
|
|
||||||
* @param {Array} results - Résultats de la recherche
|
|
||||||
* @param {string} lastName - Nom recherché
|
|
||||||
* @param {string} firstName - Prénom recherché
|
|
||||||
* @returns {Object} Résumé formaté
|
|
||||||
*/
|
|
||||||
function generateBodaccSummary(results, lastName, firstName) {
|
|
||||||
const totalResults = results.length;
|
|
||||||
const highConfidenceResults = results.filter(r => r.matchScore > 0.7);
|
|
||||||
const recentResults = results.filter(r => {
|
|
||||||
const resultDate = new Date(r.date);
|
|
||||||
const oneYearAgo = new Date();
|
|
||||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
||||||
return resultDate > oneYearAgo;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
searchTarget: `${firstName} ${lastName}`.trim(),
|
|
||||||
totalResults,
|
|
||||||
highConfidenceResults: highConfidenceResults.length,
|
|
||||||
recentResults: recentResults.length,
|
|
||||||
hasGelAvoirs: totalResults > 0,
|
|
||||||
riskLevel: totalResults === 0 ? 'Aucun' :
|
|
||||||
highConfidenceResults.length > 0 ? 'Élevé' :
|
|
||||||
totalResults > 0 ? 'Moyen' : 'Faible',
|
|
||||||
summary: totalResults === 0 ?
|
|
||||||
'Aucune mention de gel des avoirs trouvée sur Bodacc' :
|
|
||||||
`${totalResults} mention(s) trouvée(s), ${highConfidenceResults.length} avec haute confiance`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,395 +0,0 @@
|
|||||||
/**
|
|
||||||
* Collecteur GéoFoncier
|
|
||||||
* Accès aux données foncières et immobilières via l'API GéoFoncier
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
const GEOFONCIER_BASE_URL = 'https://api2.geofoncier.fr';
|
|
||||||
const GEOFONCIER_EXPERT_URL = 'https://site-expert.geofoncier.fr';
|
|
||||||
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
|
|
||||||
const REQUEST_TIMEOUT_MS = 20000; // 20 secondes timeout
|
|
||||||
const REQUEST_DELAY_MS = 2000; // 2 secondes entre les requêtes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les informations foncières pour une adresse
|
|
||||||
* @param {Object} address - Adresse à rechercher
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche GéoFoncier
|
|
||||||
*/
|
|
||||||
async function searchGeofoncierInfo(address) {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[GéoFoncier] Recherche info foncière pour: ${address.street}, ${address.city}`);
|
|
||||||
|
|
||||||
// Étape 1: Géocodage de l'adresse
|
|
||||||
const geocodeResult = await geocodeAddress(address);
|
|
||||||
|
|
||||||
if (!geocodeResult.success || !geocodeResult.coordinates) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
duration: Date.now() - startTime,
|
|
||||||
error: 'Géocodage échoué',
|
|
||||||
address,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Étape 2: Recherche des parcelles cadastrales
|
|
||||||
const parcellesResult = await searchParcelles(geocodeResult.coordinates);
|
|
||||||
|
|
||||||
// Délai de politesse
|
|
||||||
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
||||||
|
|
||||||
// Étape 3: Recherche des informations foncières
|
|
||||||
const foncierResult = await searchInfoFonciere(geocodeResult.coordinates);
|
|
||||||
|
|
||||||
// Délai de politesse
|
|
||||||
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
||||||
|
|
||||||
// Étape 4: Recherche des mutations récentes
|
|
||||||
const mutationsResult = await searchMutations(geocodeResult.coordinates);
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`[GéoFoncier] Recherche terminée en ${duration}ms`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
address,
|
|
||||||
geocode: geocodeResult,
|
|
||||||
parcelles: parcellesResult,
|
|
||||||
infoFonciere: foncierResult,
|
|
||||||
mutations: mutationsResult,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
source: 'geofoncier.fr'
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`[GéoFoncier] Erreur recherche:`, error.message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
duration,
|
|
||||||
address,
|
|
||||||
error: error.message,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Géocode une adresse via GéoFoncier
|
|
||||||
* @param {Object} address - Adresse à géocoder
|
|
||||||
* @returns {Promise<Object>} Résultat du géocodage
|
|
||||||
*/
|
|
||||||
async function geocodeAddress(address) {
|
|
||||||
try {
|
|
||||||
const query = `${address.street}, ${address.postalCode} ${address.city}`;
|
|
||||||
const geocodeUrl = `${GEOFONCIER_BASE_URL}/geocoding/search?q=${encodeURIComponent(query)}&limit=1`;
|
|
||||||
|
|
||||||
const response = await fetch(geocodeUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.features && data.features.length > 0) {
|
|
||||||
const feature = data.features[0];
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
coordinates: {
|
|
||||||
lat: feature.geometry.coordinates[1],
|
|
||||||
lon: feature.geometry.coordinates[0]
|
|
||||||
},
|
|
||||||
label: feature.properties.label,
|
|
||||||
score: feature.properties.score || 0,
|
|
||||||
data: feature.properties
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Aucun résultat de géocodage'
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les parcelles cadastrales
|
|
||||||
* @param {Object} coordinates - Coordonnées {lat, lon}
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche de parcelles
|
|
||||||
*/
|
|
||||||
async function searchParcelles(coordinates) {
|
|
||||||
try {
|
|
||||||
const parcellesUrl = `${GEOFONCIER_BASE_URL}/cadastre/parcelles?lat=${coordinates.lat}&lon=${coordinates.lon}&radius=100`;
|
|
||||||
|
|
||||||
const response = await fetch(parcellesUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const parcelles = [];
|
|
||||||
if (data.parcelles && Array.isArray(data.parcelles)) {
|
|
||||||
for (const parcelle of data.parcelles) {
|
|
||||||
parcelles.push({
|
|
||||||
numero: parcelle.numero || '',
|
|
||||||
section: parcelle.section || '',
|
|
||||||
commune: parcelle.commune || '',
|
|
||||||
surface: parcelle.surface || 0,
|
|
||||||
nature: parcelle.nature || '',
|
|
||||||
adresse: parcelle.adresse || '',
|
|
||||||
proprietaire: parcelle.proprietaire || '',
|
|
||||||
dateMutation: parcelle.dateMutation || '',
|
|
||||||
valeur: parcelle.valeur || 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
parcelles,
|
|
||||||
total: parcelles.length
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
parcelles: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les informations foncières
|
|
||||||
* @param {Object} coordinates - Coordonnées {lat, lon}
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche foncière
|
|
||||||
*/
|
|
||||||
async function searchInfoFonciere(coordinates) {
|
|
||||||
try {
|
|
||||||
const foncierUrl = `${GEOFONCIER_EXPERT_URL}/api/foncier/info?lat=${coordinates.lat}&lon=${coordinates.lon}`;
|
|
||||||
|
|
||||||
const response = await fetch(foncierUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
info: {
|
|
||||||
valeurFonciere: data.valeurFonciere || 0,
|
|
||||||
valeurLocative: data.valeurLocative || 0,
|
|
||||||
surfaceHabitable: data.surfaceHabitable || 0,
|
|
||||||
surfaceTerrain: data.surfaceTerrain || 0,
|
|
||||||
nombrePieces: data.nombrePieces || 0,
|
|
||||||
anneeConstruction: data.anneeConstruction || '',
|
|
||||||
typeHabitation: data.typeHabitation || '',
|
|
||||||
energie: data.energie || '',
|
|
||||||
ges: data.ges || '',
|
|
||||||
diagnostics: data.diagnostics || []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
info: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les mutations récentes
|
|
||||||
* @param {Object} coordinates - Coordonnées {lat, lon}
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche de mutations
|
|
||||||
*/
|
|
||||||
async function searchMutations(coordinates) {
|
|
||||||
try {
|
|
||||||
const mutationsUrl = `${GEOFONCIER_BASE_URL}/mutations/recentes?lat=${coordinates.lat}&lon=${coordinates.lon}&radius=200&years=5`;
|
|
||||||
|
|
||||||
const response = await fetch(mutationsUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const mutations = [];
|
|
||||||
if (data.mutations && Array.isArray(data.mutations)) {
|
|
||||||
for (const mutation of data.mutations) {
|
|
||||||
mutations.push({
|
|
||||||
date: mutation.date || '',
|
|
||||||
type: mutation.type || '',
|
|
||||||
valeur: mutation.valeur || 0,
|
|
||||||
surface: mutation.surface || 0,
|
|
||||||
prixM2: mutation.prixM2 || 0,
|
|
||||||
vendeur: mutation.vendeur || '',
|
|
||||||
acheteur: mutation.acheteur || '',
|
|
||||||
adresse: mutation.adresse || '',
|
|
||||||
reference: mutation.reference || ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
mutations,
|
|
||||||
total: mutations.length,
|
|
||||||
periode: '5 dernières années'
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
mutations: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les informations de zonage
|
|
||||||
* @param {Object} coordinates - Coordonnées {lat, lon}
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche de zonage
|
|
||||||
*/
|
|
||||||
async function searchZonage(coordinates) {
|
|
||||||
try {
|
|
||||||
const zonageUrl = `${GEOFONCIER_BASE_URL}/zonage/info?lat=${coordinates.lat}&lon=${coordinates.lon}`;
|
|
||||||
|
|
||||||
const response = await fetch(zonageUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
zonage: {
|
|
||||||
zone: data.zone || '',
|
|
||||||
sousZone: data.sousZone || '',
|
|
||||||
constructibilite: data.constructibilite || '',
|
|
||||||
hauteurMax: data.hauteurMax || '',
|
|
||||||
densiteMax: data.densiteMax || '',
|
|
||||||
servitudes: data.servitudes || [],
|
|
||||||
restrictions: data.restrictions || []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
zonage: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les informations de voirie
|
|
||||||
* @param {Object} coordinates - Coordonnées {lat, lon}
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche de voirie
|
|
||||||
*/
|
|
||||||
async function searchVoirie(coordinates) {
|
|
||||||
try {
|
|
||||||
const voirieUrl = `${GEOFONCIER_BASE_URL}/voirie/info?lat=${coordinates.lat}&lon=${coordinates.lon}`;
|
|
||||||
|
|
||||||
const response = await fetch(voirieUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
voirie: {
|
|
||||||
type: data.type || '',
|
|
||||||
largeur: data.largeur || 0,
|
|
||||||
revetement: data.revetement || '',
|
|
||||||
eclairage: data.eclairage || false,
|
|
||||||
canalisations: data.canalisations || [],
|
|
||||||
transports: data.transports || []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
voirie: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
searchGeofoncierInfo,
|
|
||||||
geocodeAddress,
|
|
||||||
searchParcelles,
|
|
||||||
searchInfoFonciere,
|
|
||||||
searchMutations,
|
|
||||||
searchZonage,
|
|
||||||
searchVoirie
|
|
||||||
};
|
|
||||||
@ -1,430 +0,0 @@
|
|||||||
/**
|
|
||||||
* Collecteur Inforgreffe/Societe.com - Informations entreprises
|
|
||||||
* Scraping léger avec politesse pour récupérer les données d'entreprises
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
const { JSDOM } = require('jsdom');
|
|
||||||
|
|
||||||
const SOCIETE_COM_BASE_URL = 'https://www.societe.com';
|
|
||||||
const INFORGREFFE_BASE_URL = 'https://www.inforgreffe.com';
|
|
||||||
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
|
|
||||||
const REQUEST_DELAY_MS = 1500; // 1.5 secondes entre les requêtes
|
|
||||||
const REQUEST_TIMEOUT_MS = 12000; // 12 secondes timeout
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche une entreprise sur Societe.com et Inforgreffe
|
|
||||||
* @param {string} companyName - Nom de l'entreprise
|
|
||||||
* @param {string} siren - SIREN (optionnel)
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche
|
|
||||||
*/
|
|
||||||
async function searchCompanyInfo(companyName, siren = '') {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[Inforgreffe] Recherche entreprise: ${companyName} ${siren ? `(SIREN: ${siren})` : ''}`);
|
|
||||||
|
|
||||||
// Recherche sur Societe.com d'abord (plus accessible)
|
|
||||||
const societeComResult = await searchSocieteCom(companyName, siren);
|
|
||||||
|
|
||||||
// Délai de politesse
|
|
||||||
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
||||||
|
|
||||||
// Recherche sur Inforgreffe si SIREN disponible
|
|
||||||
let inforgreffeResult = null;
|
|
||||||
if (siren) {
|
|
||||||
inforgreffeResult = await searchInforgreffe(siren);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fusion des résultats
|
|
||||||
const mergedResult = mergeCompanyResults(societeComResult, inforgreffeResult, companyName);
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`[Inforgreffe] Recherche terminée en ${duration}ms`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
company: mergedResult,
|
|
||||||
sources: {
|
|
||||||
societeCom: societeComResult,
|
|
||||||
inforgreffe: inforgreffeResult
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`[Inforgreffe] Erreur recherche:`, error.message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
duration,
|
|
||||||
error: error.message,
|
|
||||||
company: null,
|
|
||||||
sources: {},
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche sur Societe.com
|
|
||||||
* @param {string} companyName - Nom de l'entreprise
|
|
||||||
* @param {string} siren - SIREN (optionnel)
|
|
||||||
* @returns {Promise<Object>} Résultat Societe.com
|
|
||||||
*/
|
|
||||||
async function searchSocieteCom(companyName, siren = '') {
|
|
||||||
try {
|
|
||||||
// Construction de l'URL de recherche
|
|
||||||
const searchQuery = siren || companyName;
|
|
||||||
const searchUrl = `${SOCIETE_COM_BASE_URL}/search?q=${encodeURIComponent(searchQuery)}`;
|
|
||||||
|
|
||||||
const response = await fetch(searchUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
|
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
'Upgrade-Insecure-Requests': '1'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = await response.text();
|
|
||||||
const dom = new JSDOM(html);
|
|
||||||
const document = dom.window.document;
|
|
||||||
|
|
||||||
return extractSocieteComData(document, companyName, siren);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[Societe.com] Erreur:`, error.message);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
data: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche sur Inforgreffe
|
|
||||||
* @param {string} siren - SIREN de l'entreprise
|
|
||||||
* @returns {Promise<Object>} Résultat Inforgreffe
|
|
||||||
*/
|
|
||||||
async function searchInforgreffe(siren) {
|
|
||||||
try {
|
|
||||||
// URL de recherche par SIREN
|
|
||||||
const searchUrl = `${INFORGREFFE_BASE_URL}/entreprise/${siren}`;
|
|
||||||
|
|
||||||
const response = await fetch(searchUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
|
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
'Upgrade-Insecure-Requests': '1'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const html = await response.text();
|
|
||||||
const dom = new JSDOM(html);
|
|
||||||
const document = dom.window.document;
|
|
||||||
|
|
||||||
return extractInforgreffeData(document, siren);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[Inforgreffe] Erreur:`, error.message);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
data: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extrait les données de Societe.com
|
|
||||||
* @param {Document} document - Document DOM parsé
|
|
||||||
* @param {string} companyName - Nom de l'entreprise
|
|
||||||
* @param {string} siren - SIREN
|
|
||||||
* @returns {Object} Données extraites
|
|
||||||
*/
|
|
||||||
function extractSocieteComData(document, companyName, siren) {
|
|
||||||
try {
|
|
||||||
const data = {
|
|
||||||
name: companyName,
|
|
||||||
siren: siren,
|
|
||||||
siret: '',
|
|
||||||
forme: '',
|
|
||||||
capital: '',
|
|
||||||
adresse: '',
|
|
||||||
dirigeants: [],
|
|
||||||
activite: '',
|
|
||||||
dateCreation: '',
|
|
||||||
sourceUrl: SOCIETE_COM_BASE_URL
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extraction du nom de l'entreprise
|
|
||||||
const nameElement = document.querySelector('.company-name, .nom-entreprise, h1, .title');
|
|
||||||
if (nameElement) {
|
|
||||||
data.name = nameElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction du SIREN/SIRET
|
|
||||||
const sirenElement = document.querySelector('.siren, .num-siren, [data-siren]');
|
|
||||||
if (sirenElement) {
|
|
||||||
const sirenText = sirenElement.textContent || sirenElement.getAttribute('data-siren');
|
|
||||||
data.siren = extractSiren(sirenText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction de la forme juridique
|
|
||||||
const formeElement = document.querySelector('.forme, .forme-juridique, .legal-form');
|
|
||||||
if (formeElement) {
|
|
||||||
data.forme = formeElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction du capital
|
|
||||||
const capitalElement = document.querySelector('.capital, .capital-social, .share-capital');
|
|
||||||
if (capitalElement) {
|
|
||||||
data.capital = capitalElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction de l'adresse
|
|
||||||
const adresseElement = document.querySelector('.adresse, .address, .company-address');
|
|
||||||
if (adresseElement) {
|
|
||||||
data.adresse = adresseElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des dirigeants
|
|
||||||
const dirigeantsElements = document.querySelectorAll('.dirigeant, .manager, .president, .gérant');
|
|
||||||
dirigeantsElements.forEach(element => {
|
|
||||||
const name = element.textContent.trim();
|
|
||||||
if (name && name.length > 2) {
|
|
||||||
data.dirigeants.push({
|
|
||||||
nom: name,
|
|
||||||
fonction: 'Dirigeant',
|
|
||||||
source: 'societe.com'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Extraction de l'activité
|
|
||||||
const activiteElement = document.querySelector('.activite, .activity, .secteur');
|
|
||||||
if (activiteElement) {
|
|
||||||
data.activite = activiteElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction de la date de création
|
|
||||||
const dateElement = document.querySelector('.date-creation, .creation-date, .date-creation-entreprise');
|
|
||||||
if (dateElement) {
|
|
||||||
data.dateCreation = dateElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[Societe.com] Erreur extraction:`, error.message);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
data: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extrait les données d'Inforgreffe
|
|
||||||
* @param {Document} document - Document DOM parsé
|
|
||||||
* @param {string} siren - SIREN
|
|
||||||
* @returns {Object} Données extraites
|
|
||||||
*/
|
|
||||||
function extractInforgreffeData(document, siren) {
|
|
||||||
try {
|
|
||||||
const data = {
|
|
||||||
siren: siren,
|
|
||||||
name: '',
|
|
||||||
siret: '',
|
|
||||||
forme: '',
|
|
||||||
capital: '',
|
|
||||||
adresse: '',
|
|
||||||
dirigeants: [],
|
|
||||||
activite: '',
|
|
||||||
dateCreation: '',
|
|
||||||
sourceUrl: `${INFORGREFFE_BASE_URL}/entreprise/${siren}`
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extraction du nom de l'entreprise
|
|
||||||
const nameElement = document.querySelector('.company-name, .nom-entreprise, h1, .title');
|
|
||||||
if (nameElement) {
|
|
||||||
data.name = nameElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction du SIRET
|
|
||||||
const siretElement = document.querySelector('.siret, .num-siret, [data-siret]');
|
|
||||||
if (siretElement) {
|
|
||||||
data.siret = siretElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction de la forme juridique
|
|
||||||
const formeElement = document.querySelector('.forme, .forme-juridique, .legal-form');
|
|
||||||
if (formeElement) {
|
|
||||||
data.forme = formeElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction du capital
|
|
||||||
const capitalElement = document.querySelector('.capital, .capital-social, .share-capital');
|
|
||||||
if (capitalElement) {
|
|
||||||
data.capital = capitalElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction de l'adresse
|
|
||||||
const adresseElement = document.querySelector('.adresse, .address, .company-address');
|
|
||||||
if (adresseElement) {
|
|
||||||
data.adresse = adresseElement.textContent.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des dirigeants
|
|
||||||
const dirigeantsElements = document.querySelectorAll('.dirigeant, .manager, .president, .gérant');
|
|
||||||
dirigeantsElements.forEach(element => {
|
|
||||||
const name = element.textContent.trim();
|
|
||||||
if (name && name.length > 2) {
|
|
||||||
data.dirigeants.push({
|
|
||||||
nom: name,
|
|
||||||
fonction: 'Dirigeant',
|
|
||||||
source: 'inforgreffe.com'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[Inforgreffe] Erreur extraction:`, error.message);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
data: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fusionne les résultats de Societe.com et Inforgreffe
|
|
||||||
* @param {Object} societeComResult - Résultat Societe.com
|
|
||||||
* @param {Object} inforgreffeResult - Résultat Inforgreffe
|
|
||||||
* @param {string} originalName - Nom original de l'entreprise
|
|
||||||
* @returns {Object} Données fusionnées
|
|
||||||
*/
|
|
||||||
function mergeCompanyResults(societeComResult, inforgreffeResult, originalName) {
|
|
||||||
const merged = {
|
|
||||||
name: originalName,
|
|
||||||
siren: '',
|
|
||||||
siret: '',
|
|
||||||
forme: '',
|
|
||||||
capital: '',
|
|
||||||
adresse: '',
|
|
||||||
dirigeants: [],
|
|
||||||
activite: '',
|
|
||||||
dateCreation: '',
|
|
||||||
sources: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fusion des données Societe.com
|
|
||||||
if (societeComResult.success && societeComResult.data) {
|
|
||||||
const sc = societeComResult.data;
|
|
||||||
merged.name = sc.name || merged.name;
|
|
||||||
merged.siren = sc.siren || merged.siren;
|
|
||||||
merged.siret = sc.siret || merged.siret;
|
|
||||||
merged.forme = sc.forme || merged.forme;
|
|
||||||
merged.capital = sc.capital || merged.capital;
|
|
||||||
merged.adresse = sc.adresse || merged.adresse;
|
|
||||||
merged.activite = sc.activite || merged.activite;
|
|
||||||
merged.dateCreation = sc.dateCreation || merged.dateCreation;
|
|
||||||
merged.dirigeants.push(...sc.dirigeants);
|
|
||||||
merged.sources.push('societe.com');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fusion des données Inforgreffe
|
|
||||||
if (inforgreffeResult && inforgreffeResult.success && inforgreffeResult.data) {
|
|
||||||
const ig = inforgreffeResult.data;
|
|
||||||
merged.name = ig.name || merged.name;
|
|
||||||
merged.siren = ig.siren || merged.siren;
|
|
||||||
merged.siret = ig.siret || merged.siret;
|
|
||||||
merged.forme = ig.forme || merged.forme;
|
|
||||||
merged.capital = ig.capital || merged.capital;
|
|
||||||
merged.adresse = ig.adresse || merged.adresse;
|
|
||||||
merged.dateCreation = ig.dateCreation || merged.dateCreation;
|
|
||||||
merged.dirigeants.push(...ig.dirigeants);
|
|
||||||
merged.sources.push('inforgreffe.com');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Déduplication des dirigeants
|
|
||||||
merged.dirigeants = merged.dirigeants.filter((dirigeant, index, self) =>
|
|
||||||
index === self.findIndex(d => d.nom === dirigeant.nom)
|
|
||||||
);
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extrait le SIREN d'un texte
|
|
||||||
* @param {string} text - Texte contenant potentiellement un SIREN
|
|
||||||
* @returns {string} SIREN extrait
|
|
||||||
*/
|
|
||||||
function extractSiren(text) {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// Recherche d'un SIREN (9 chiffres)
|
|
||||||
const sirenMatch = text.match(/\b(\d{9})\b/);
|
|
||||||
return sirenMatch ? sirenMatch[1] : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un résumé des données d'entreprise pour le PDF
|
|
||||||
* @param {Object} companyData - Données de l'entreprise
|
|
||||||
* @returns {Object} Résumé formaté
|
|
||||||
*/
|
|
||||||
function generateCompanySummary(companyData) {
|
|
||||||
return {
|
|
||||||
name: companyData.name,
|
|
||||||
siren: companyData.siren,
|
|
||||||
siret: companyData.siret,
|
|
||||||
forme: companyData.forme,
|
|
||||||
capital: companyData.capital,
|
|
||||||
adresse: companyData.adresse,
|
|
||||||
dirigeantsCount: companyData.dirigeants.length,
|
|
||||||
activite: companyData.activite,
|
|
||||||
dateCreation: companyData.dateCreation,
|
|
||||||
sources: companyData.sources,
|
|
||||||
hasCompleteInfo: !!(companyData.siren && companyData.forme && companyData.adresse),
|
|
||||||
summary: companyData.siren ?
|
|
||||||
`Entreprise trouvée: ${companyData.name} (SIREN: ${companyData.siren})` :
|
|
||||||
`Informations partielles pour: ${companyData.name}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
searchCompanyInfo,
|
|
||||||
generateCompanySummary
|
|
||||||
};
|
|
||||||
@ -1,436 +0,0 @@
|
|||||||
/**
|
|
||||||
* Générateur de PDF pour les entités enrichies
|
|
||||||
* Génère des PDF formatés pour les personnes, adresses et entreprises
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un PDF pour une personne (Bodacc - gel des avoirs)
|
|
||||||
* @param {Object} personData - Données de la personne
|
|
||||||
* @param {Object} bodaccResult - Résultat de la recherche Bodacc
|
|
||||||
* @param {string} outputPath - Chemin de sortie du PDF
|
|
||||||
* @returns {Promise<string>} Chemin du PDF généré
|
|
||||||
*/
|
|
||||||
async function generatePersonPdf(personData, bodaccResult, outputPath) {
|
|
||||||
try {
|
|
||||||
const pdfContent = generatePersonPdfContent(personData, bodaccResult);
|
|
||||||
await fs.writeFile(outputPath, pdfContent, 'utf8');
|
|
||||||
console.log(`[PDF] Personne généré: ${outputPath}`);
|
|
||||||
return outputPath;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[PDF] Erreur génération personne:`, error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un PDF pour une entreprise (Inforgreffe/Societe.com)
|
|
||||||
* @param {Object} companyData - Données de l'entreprise
|
|
||||||
* @param {Object} inforgreffeResult - Résultat de la recherche Inforgreffe
|
|
||||||
* @param {string} outputPath - Chemin de sortie du PDF
|
|
||||||
* @returns {Promise<string>} Chemin du PDF généré
|
|
||||||
*/
|
|
||||||
async function generateCompanyPdf(companyData, inforgreffeResult, outputPath) {
|
|
||||||
try {
|
|
||||||
const pdfContent = generateCompanyPdfContent(companyData, inforgreffeResult);
|
|
||||||
await fs.writeFile(outputPath, pdfContent, 'utf8');
|
|
||||||
console.log(`[PDF] Entreprise généré: ${outputPath}`);
|
|
||||||
return outputPath;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[PDF] Erreur génération entreprise:`, error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un PDF pour une adresse (Cadastre/GéoRisque)
|
|
||||||
* @param {Object} addressData - Données de l'adresse
|
|
||||||
* @param {Object} geoResult - Résultat de la recherche géographique
|
|
||||||
* @param {string} outputPath - Chemin de sortie du PDF
|
|
||||||
* @returns {Promise<string>} Chemin du PDF généré
|
|
||||||
*/
|
|
||||||
async function generateAddressPdf(addressData, geoResult, outputPath) {
|
|
||||||
try {
|
|
||||||
const pdfContent = generateAddressPdfContent(addressData, geoResult);
|
|
||||||
await fs.writeFile(outputPath, pdfContent, 'utf8');
|
|
||||||
console.log(`[PDF] Adresse généré: ${outputPath}`);
|
|
||||||
return outputPath;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[PDF] Erreur génération adresse:`, error.message);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère le contenu HTML pour le PDF d'une personne
|
|
||||||
* @param {Object} personData - Données de la personne
|
|
||||||
* @param {Object} bodaccResult - Résultat Bodacc
|
|
||||||
* @returns {string} Contenu HTML
|
|
||||||
*/
|
|
||||||
function generatePersonPdfContent(personData, bodaccResult) {
|
|
||||||
const summary = bodaccResult.summary || {};
|
|
||||||
const results = bodaccResult.results || [];
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Rapport Bodacc - ${personData.firstName} ${personData.lastName}</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
|
|
||||||
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
|
|
||||||
.title { color: #333; margin: 0; }
|
|
||||||
.subtitle { color: #666; margin: 5px 0 0 0; }
|
|
||||||
.section { margin: 20px 0; }
|
|
||||||
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
|
|
||||||
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
|
||||||
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
|
|
||||||
.info-label { font-weight: bold; color: #555; }
|
|
||||||
.info-value { margin-top: 5px; }
|
|
||||||
.risk-high { color: #e74c3c; font-weight: bold; }
|
|
||||||
.risk-medium { color: #f39c12; font-weight: bold; }
|
|
||||||
.risk-low { color: #27ae60; font-weight: bold; }
|
|
||||||
.result-item { background: #fff; border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
|
||||||
.result-date { color: #666; font-size: 0.9em; }
|
|
||||||
.result-score { background: #3498db; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
|
|
||||||
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
|
|
||||||
.no-results { text-align: center; color: #27ae60; font-style: italic; padding: 20px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1 class="title">Rapport Bodacc - Gel des avoirs</h1>
|
|
||||||
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Identité recherchée</h2>
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Nom complet</div>
|
|
||||||
<div class="info-value">${personData.firstName} ${personData.lastName}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Date de naissance</div>
|
|
||||||
<div class="info-value">${personData.birthDate || 'Non renseignée'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Résumé de la recherche</h2>
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Statut</div>
|
|
||||||
<div class="info-value class="risk-${summary.riskLevel?.toLowerCase() || 'low'}">${summary.riskLevel || 'Faible'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Résultats trouvés</div>
|
|
||||||
<div class="info-value">${summary.totalResults || 0}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Haute confiance</div>
|
|
||||||
<div class="info-value">${summary.highConfidenceResults || 0}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Récents (1 an)</div>
|
|
||||||
<div class="info-value">${summary.recentResults || 0}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p><strong>Synthèse:</strong> ${summary.summary || 'Aucune donnée disponible'}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Détail des résultats</h2>
|
|
||||||
${results.length === 0 ?
|
|
||||||
'<div class="no-results">✅ Aucune mention de gel des avoirs trouvée</div>' :
|
|
||||||
results.map(result => `
|
|
||||||
<div class="result-item">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<h3 style="margin: 0;">${result.name}</h3>
|
|
||||||
<span class="result-score">Score: ${(result.matchScore * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="result-date">Date: ${result.date}</div>
|
|
||||||
<p><strong>Description:</strong> ${result.description}</p>
|
|
||||||
<p><strong>Source:</strong> <a href="${result.sourceUrl}" target="_blank">${result.sourceUrl}</a></p>
|
|
||||||
</div>
|
|
||||||
`).join('')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p><strong>Source:</strong> ${bodaccResult.source || 'Bodacc.fr'}</p>
|
|
||||||
<p><strong>Recherche effectuée le:</strong> ${bodaccResult.timestamp ? new Date(bodaccResult.timestamp).toLocaleString('fr-FR') : 'Non disponible'}</p>
|
|
||||||
<p><strong>Durée de la recherche:</strong> ${bodaccResult.duration || 0}ms</p>
|
|
||||||
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front. Les informations sont fournies à titre indicatif.</em></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère le contenu HTML pour le PDF d'une entreprise
|
|
||||||
* @param {Object} companyData - Données de l'entreprise
|
|
||||||
* @param {Object} inforgreffeResult - Résultat Inforgreffe
|
|
||||||
* @returns {string} Contenu HTML
|
|
||||||
*/
|
|
||||||
function generateCompanyPdfContent(companyData, inforgreffeResult) {
|
|
||||||
const company = inforgreffeResult.company || {};
|
|
||||||
const sources = inforgreffeResult.sources || {};
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Rapport Entreprise - ${company.name || companyData.name}</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
|
|
||||||
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
|
|
||||||
.title { color: #333; margin: 0; }
|
|
||||||
.subtitle { color: #666; margin: 5px 0 0 0; }
|
|
||||||
.section { margin: 20px 0; }
|
|
||||||
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
|
|
||||||
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
|
||||||
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
|
|
||||||
.info-label { font-weight: bold; color: #555; }
|
|
||||||
.info-value { margin-top: 5px; }
|
|
||||||
.status-ok { color: #27ae60; font-weight: bold; }
|
|
||||||
.status-partial { color: #f39c12; font-weight: bold; }
|
|
||||||
.status-missing { color: #e74c3c; font-weight: bold; }
|
|
||||||
.dirigeant-item { background: #fff; border: 1px solid #ddd; padding: 10px; margin: 5px 0; border-radius: 5px; }
|
|
||||||
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1 class="title">Rapport Entreprise</h1>
|
|
||||||
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Informations générales</h2>
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Raison sociale</div>
|
|
||||||
<div class="info-value">${company.name || companyData.name || 'Non renseignée'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">SIREN</div>
|
|
||||||
<div class="info-value class="status-${company.siren ? 'ok' : 'missing'}">${company.siren || 'Non trouvé'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">SIRET</div>
|
|
||||||
<div class="info-value class="status-${company.siret ? 'ok' : 'missing'}">${company.siret || 'Non trouvé'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Forme juridique</div>
|
|
||||||
<div class="info-value class="status-${company.forme ? 'ok' : 'missing'}">${company.forme || 'Non renseignée'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Capital social</div>
|
|
||||||
<div class="info-value class="status-${company.capital ? 'ok' : 'missing'}">${company.capital || 'Non renseigné'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Date de création</div>
|
|
||||||
<div class="info-value class="status-${company.dateCreation ? 'ok' : 'missing'}">${company.dateCreation || 'Non renseignée'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Adresse</h2>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Adresse complète</div>
|
|
||||||
<div class="info-value class="status-${company.adresse ? 'ok' : 'missing'}">${company.adresse || 'Non renseignée'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Activité</h2>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Secteur d'activité</div>
|
|
||||||
<div class="info-value class="status-${company.activite ? 'ok' : 'missing'}">${company.activite || 'Non renseignée'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Dirigeants (${company.dirigeants?.length || 0})</h2>
|
|
||||||
${company.dirigeants && company.dirigeants.length > 0 ?
|
|
||||||
company.dirigeants.map(dirigeant => `
|
|
||||||
<div class="dirigeant-item">
|
|
||||||
<strong>${dirigeant.nom}</strong> - ${dirigeant.fonction || 'Dirigeant'}
|
|
||||||
<br><small>Source: ${dirigeant.source || 'Non spécifiée'}</small>
|
|
||||||
</div>
|
|
||||||
`).join('') :
|
|
||||||
'<p class="status-missing">Aucun dirigeant trouvé</p>'
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Sources consultées</h2>
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Societe.com</div>
|
|
||||||
<div class="info-value class="status-${sources.societeCom?.success ? 'ok' : 'missing'}">${sources.societeCom?.success ? '✅ Consulté' : '❌ Non accessible'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Inforgreffe</div>
|
|
||||||
<div class="info-value class="status-${sources.inforgreffe?.success ? 'ok' : 'missing'}">${sources.inforgreffe?.success ? '✅ Consulté' : '❌ Non accessible'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p><strong>Recherche effectuée le:</strong> ${inforgreffeResult.timestamp ? new Date(inforgreffeResult.timestamp).toLocaleString('fr-FR') : 'Non disponible'}</p>
|
|
||||||
<p><strong>Durée de la recherche:</strong> ${inforgreffeResult.duration || 0}ms</p>
|
|
||||||
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front. Les informations sont fournies à titre indicatif.</em></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère le contenu HTML pour le PDF d'une adresse
|
|
||||||
* @param {Object} addressData - Données de l'adresse
|
|
||||||
* @param {Object} geoResult - Résultat géographique
|
|
||||||
* @returns {string} Contenu HTML
|
|
||||||
*/
|
|
||||||
function generateAddressPdfContent(addressData, geoResult) {
|
|
||||||
const geocode = geoResult.geocode || {}
|
|
||||||
const risks = geoResult.risks || []
|
|
||||||
const cadastre = geoResult.cadastre || []
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Rapport Adresse - ${addressData.street || 'Adresse'}</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
|
|
||||||
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
|
|
||||||
.title { color: #333; margin: 0; }
|
|
||||||
.subtitle { color: #666; margin: 5px 0 0 0; }
|
|
||||||
.section { margin: 20px 0; }
|
|
||||||
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
|
|
||||||
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
|
||||||
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
|
|
||||||
.info-label { font-weight: bold; color: #555; }
|
|
||||||
.info-value { margin-top: 5px; }
|
|
||||||
.status-ok { color: #27ae60; font-weight: bold; }
|
|
||||||
.status-error { color: #e74c3c; font-weight: bold; }
|
|
||||||
.risk-item { background: #fff; border: 1px solid #ddd; padding: 10px; margin: 5px 0; border-radius: 5px; }
|
|
||||||
.risk-high { border-left: 4px solid #e74c3c; }
|
|
||||||
.risk-medium { border-left: 4px solid #f39c12; }
|
|
||||||
.risk-low { border-left: 4px solid #27ae60; }
|
|
||||||
.parcelle-item { background: #fff; border: 1px solid #ddd; padding: 10px; margin: 5px 0; border-radius: 5px; }
|
|
||||||
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
|
|
||||||
.no-data { text-align: center; color: #666; font-style: italic; padding: 20px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1 class="title">Rapport Adresse</h1>
|
|
||||||
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Adresse analysée</h2>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Adresse complète</div>
|
|
||||||
<div class="info-value">${addressData.street || ''} ${addressData.postalCode || ''} ${addressData.city || ''} ${addressData.country || ''}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Géocodage</h2>
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Statut</div>
|
|
||||||
<div class="info-value class="status-${geocode.success ? 'ok' : 'error'}">${geocode.success ? '✅ Géocodé' : '❌ Échec'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Score de confiance</div>
|
|
||||||
<div class="info-value">${geocode.score ? (geocode.score * 100).toFixed(0) + '%' : 'N/A'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Coordonnées</div>
|
|
||||||
<div class="info-value">${geocode.lat && geocode.lon ? `${geocode.lat}, ${geocode.lon}` : 'Non disponibles'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Adresse normalisée</div>
|
|
||||||
<div class="info-value">${geocode.label || 'Non disponible'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Risques majeurs (${risks.length})</h2>
|
|
||||||
${risks.length === 0 ?
|
|
||||||
'<div class="no-data">✅ Aucun risque majeur identifié dans un rayon de 1km</div>' :
|
|
||||||
risks.map(risk => `
|
|
||||||
<div class="risk-item risk-${risk.level?.toLowerCase() || 'low'}">
|
|
||||||
<h3 style="margin: 0;">${risk.type}</h3>
|
|
||||||
<p><strong>Niveau:</strong> ${risk.level}</p>
|
|
||||||
<p><strong>Description:</strong> ${risk.description}</p>
|
|
||||||
${risk.distance ? `<p><strong>Distance:</strong> ${risk.distance}m</p>` : ''}
|
|
||||||
<p><strong>Source:</strong> <a href="${risk.sourceUrl}" target="_blank">GéoRisque</a></p>
|
|
||||||
</div>
|
|
||||||
`).join('')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Informations cadastrales (${cadastre.length})</h2>
|
|
||||||
${cadastre.length === 0 ?
|
|
||||||
'<div class="no-data">Aucune parcelle cadastrale trouvée</div>' :
|
|
||||||
cadastre.map(parcelle => `
|
|
||||||
<div class="parcelle-item">
|
|
||||||
<h3 style="margin: 0;">Parcelle ${parcelle.section}${parcelle.numero}</h3>
|
|
||||||
<p><strong>Commune:</strong> ${parcelle.commune}</p>
|
|
||||||
${parcelle.surface ? `<p><strong>Surface:</strong> ${parcelle.surface}m²</p>` : ''}
|
|
||||||
<p><strong>Source:</strong> <a href="${parcelle.sourceUrl}" target="_blank">Cadastre</a></p>
|
|
||||||
</div>
|
|
||||||
`).join('')
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Sources consultées</h2>
|
|
||||||
<div class="info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Base Adresse Nationale</div>
|
|
||||||
<div class="info-value class="status-${geocode.success ? 'ok' : 'error'}">${geocode.success ? '✅ Consultée' : '❌ Non accessible'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">GéoRisque</div>
|
|
||||||
<div class="info-value class="status-${risks.length >= 0 ? 'ok' : 'error'}">✅ Consulté</div>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Cadastre</div>
|
|
||||||
<div class="info-value class="status-${cadastre.length >= 0 ? 'ok' : 'error'}">✅ Consulté</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p><strong>Recherche effectuée le:</strong> ${geoResult.timestamp ? new Date(geoResult.timestamp).toLocaleString('fr-FR') : 'Non disponible'}</p>
|
|
||||||
<p><strong>Sources:</strong> ${(geoResult.sources || []).join(', ')}</p>
|
|
||||||
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front. Les informations sont fournies à titre indicatif.</em></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
generatePersonPdf,
|
|
||||||
generateCompanyPdf,
|
|
||||||
generateAddressPdf
|
|
||||||
};
|
|
||||||
@ -1,287 +0,0 @@
|
|||||||
/**
|
|
||||||
* Collecteur RBE (Registre des Bénéficiaires Effectifs)
|
|
||||||
* Accès aux données des bénéficiaires effectifs via l'API RBE
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
|
|
||||||
const RBE_BASE_URL = 'https://registre-beneficiaires-effectifs.inpi.fr';
|
|
||||||
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
|
|
||||||
const REQUEST_TIMEOUT_MS = 15000; // 15 secondes timeout
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les bénéficiaires effectifs pour une entreprise
|
|
||||||
* @param {string} siren - SIREN de l'entreprise
|
|
||||||
* @param {string} siret - SIRET de l'entreprise (optionnel)
|
|
||||||
* @returns {Promise<Object>} Résultat de la recherche RBE
|
|
||||||
*/
|
|
||||||
async function searchRBEBeneficiaires(siren, siret = '') {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[RBE] Recherche bénéficiaires effectifs pour SIREN: ${siren}`);
|
|
||||||
|
|
||||||
// Vérification de la validité du SIREN
|
|
||||||
if (!siren || siren.length !== 9 || !/^\d{9}$/.test(siren)) {
|
|
||||||
throw new Error('SIREN invalide - doit contenir 9 chiffres');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construction de l'URL de recherche
|
|
||||||
const searchUrl = `${RBE_BASE_URL}/api/beneficiaires?q=${encodeURIComponent(siren)}`;
|
|
||||||
|
|
||||||
// Requête avec headers appropriés
|
|
||||||
const response = await fetch(searchUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
|
|
||||||
'Cache-Control': 'no-cache'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 404) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration: Date.now() - startTime,
|
|
||||||
siren,
|
|
||||||
beneficiaires: [],
|
|
||||||
message: 'Aucun bénéficiaire effectif trouvé',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Traitement des données RBE
|
|
||||||
const beneficiaires = processRBEData(data, siren);
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`[RBE] Recherche terminée en ${duration}ms - ${beneficiaires.length} bénéficiaire(s) trouvé(s)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
siren,
|
|
||||||
siret,
|
|
||||||
beneficiaires,
|
|
||||||
total: beneficiaires.length,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
source: 'rbe.inpi.fr'
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`[RBE] Erreur recherche:`, error.message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
duration,
|
|
||||||
siren,
|
|
||||||
error: error.message,
|
|
||||||
beneficiaires: [],
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Traite les données RBE brutes
|
|
||||||
* @param {Object} data - Données brutes de l'API RBE
|
|
||||||
* @param {string} siren - SIREN de l'entreprise
|
|
||||||
* @returns {Array} Liste des bénéficiaires effectifs
|
|
||||||
*/
|
|
||||||
function processRBEData(data, siren) {
|
|
||||||
try {
|
|
||||||
const beneficiaires = [];
|
|
||||||
|
|
||||||
// Structure des données RBE (à adapter selon l'API réelle)
|
|
||||||
if (data.beneficiaires && Array.isArray(data.beneficiaires)) {
|
|
||||||
for (const benef of data.beneficiaires) {
|
|
||||||
const beneficiaire = {
|
|
||||||
nom: benef.nom || '',
|
|
||||||
prenom: benef.prenom || '',
|
|
||||||
dateNaissance: benef.dateNaissance || '',
|
|
||||||
nationalite: benef.nationalite || '',
|
|
||||||
adresse: benef.adresse || '',
|
|
||||||
qualite: benef.qualite || '',
|
|
||||||
pourcentage: benef.pourcentage || 0,
|
|
||||||
type: benef.type || 'personne_physique', // personne_physique ou personne_morale
|
|
||||||
siren: benef.siren || '',
|
|
||||||
dateDeclaration: benef.dateDeclaration || '',
|
|
||||||
statut: benef.statut || 'actif'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validation des données
|
|
||||||
if (beneficiaire.nom || beneficiaire.prenom) {
|
|
||||||
beneficiaires.push(beneficiaire);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return beneficiaires;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[RBE] Erreur traitement données:`, error.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les informations détaillées d'un bénéficiaire
|
|
||||||
* @param {string} beneficiaireId - ID du bénéficiaire
|
|
||||||
* @returns {Promise<Object>} Informations détaillées
|
|
||||||
*/
|
|
||||||
async function getBeneficiaireDetails(beneficiaireId) {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[RBE] Recherche détails bénéficiaire: ${beneficiaireId}`);
|
|
||||||
|
|
||||||
const detailsUrl = `${RBE_BASE_URL}/api/beneficiaires/${encodeURIComponent(beneficiaireId)}`;
|
|
||||||
|
|
||||||
const response = await fetch(detailsUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`[RBE] Détails récupérés en ${duration}ms`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
beneficiaireId,
|
|
||||||
details: data,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`[RBE] Erreur détails:`, error.message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
duration,
|
|
||||||
beneficiaireId,
|
|
||||||
error: error.message,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recherche les entreprises liées à une personne
|
|
||||||
* @param {string} nom - Nom de famille
|
|
||||||
* @param {string} prenom - Prénom (optionnel)
|
|
||||||
* @returns {Promise<Object>} Liste des entreprises
|
|
||||||
*/
|
|
||||||
async function searchPersonneEntreprises(nom, prenom = '') {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[RBE] Recherche entreprises pour: ${nom} ${prenom}`);
|
|
||||||
|
|
||||||
const searchQuery = `${nom} ${prenom}`.trim();
|
|
||||||
const searchUrl = `${RBE_BASE_URL}/api/personnes?q=${encodeURIComponent(searchQuery)}`;
|
|
||||||
|
|
||||||
const response = await fetch(searchUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': USER_AGENT,
|
|
||||||
'Accept': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: REQUEST_TIMEOUT_MS
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const entreprises = processPersonneEntreprises(data);
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.log(`[RBE] Recherche terminée en ${duration}ms - ${entreprises.length} entreprise(s) trouvée(s)`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
duration,
|
|
||||||
nom,
|
|
||||||
prenom,
|
|
||||||
entreprises,
|
|
||||||
total: entreprises.length,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
console.error(`[RBE] Erreur recherche personne:`, error.message);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
duration,
|
|
||||||
nom,
|
|
||||||
prenom,
|
|
||||||
error: error.message,
|
|
||||||
entreprises: [],
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Traite les données d'entreprises liées à une personne
|
|
||||||
* @param {Object} data - Données brutes
|
|
||||||
* @returns {Array} Liste des entreprises
|
|
||||||
*/
|
|
||||||
function processPersonneEntreprises(data) {
|
|
||||||
try {
|
|
||||||
const entreprises = [];
|
|
||||||
|
|
||||||
if (data.entreprises && Array.isArray(data.entreprises)) {
|
|
||||||
for (const ent of data.entreprises) {
|
|
||||||
const entreprise = {
|
|
||||||
siren: ent.siren || '',
|
|
||||||
denomination: ent.denomination || '',
|
|
||||||
forme: ent.forme || '',
|
|
||||||
adresse: ent.adresse || '',
|
|
||||||
qualite: ent.qualite || '',
|
|
||||||
pourcentage: ent.pourcentage || 0,
|
|
||||||
dateDeclaration: ent.dateDeclaration || '',
|
|
||||||
statut: ent.statut || 'actif'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (entreprise.siren && entreprise.denomination) {
|
|
||||||
entreprises.push(entreprise);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entreprises;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[RBE] Erreur traitement entreprises:`, error.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
searchRBEBeneficiaires,
|
|
||||||
getBeneficiaireDetails,
|
|
||||||
searchPersonneEntreprises
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,293 +0,0 @@
|
|||||||
const sharp = require('sharp')
|
|
||||||
const path = require('path')
|
|
||||||
const fs = require('fs')
|
|
||||||
const { execSync } = require('child_process')
|
|
||||||
const {
|
|
||||||
isCNIDocument,
|
|
||||||
enhanceCNIPreprocessing,
|
|
||||||
processCNIWithZones,
|
|
||||||
decodeMRZ,
|
|
||||||
} = require('./cniOcrEnhancer')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OCR amélioré avec support spécialisé pour les CNI
|
|
||||||
* Combine Tesseract avec des techniques de preprocessing avancées
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Fonction pour exécuter Tesseract avec stratégies multiples et choisir le meilleur
|
|
||||||
async function runTesseractOCR(imageBuffer, options = {}) {
|
|
||||||
const tempInput = path.join(__dirname, `temp_input_${Date.now()}.png`)
|
|
||||||
const tempOutputBase = path.join(__dirname, `temp_output_${Date.now()}`)
|
|
||||||
fs.writeFileSync(tempInput, imageBuffer)
|
|
||||||
|
|
||||||
const strategies = []
|
|
||||||
const baseLang = options.language || 'fra'
|
|
||||||
const basePsm = options.psm || '6'
|
|
||||||
const baseOem = options.oem || '3'
|
|
||||||
|
|
||||||
// Stratégies génériques
|
|
||||||
strategies.push({ lang: baseLang, psm: basePsm, oem: baseOem })
|
|
||||||
strategies.push({ lang: baseLang, psm: '3', oem: baseOem })
|
|
||||||
strategies.push({ lang: baseLang, psm: '13', oem: baseOem })
|
|
||||||
|
|
||||||
// Si on cible MRZ/OCRB
|
|
||||||
if ((options.language || '').includes('eng') || options.mrz) {
|
|
||||||
// OCRB peut ne pas être installé; on tente eng+ocrb puis eng seul
|
|
||||||
strategies.push({ lang: 'ocrb+eng', psm: '6', oem: baseOem })
|
|
||||||
strategies.push({ lang: 'ocrb+eng', psm: '8', oem: baseOem })
|
|
||||||
strategies.push({ lang: 'eng', psm: '6', oem: baseOem })
|
|
||||||
strategies.push({ lang: 'eng', psm: '8', oem: baseOem })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stratégies spécialisées pour CNI (noms français)
|
|
||||||
if (options.cni || options.frenchNames) {
|
|
||||||
strategies.push({ lang: 'fra', psm: '6', oem: baseOem })
|
|
||||||
strategies.push({ lang: 'fra', psm: '8', oem: baseOem })
|
|
||||||
strategies.push({ lang: 'fra', psm: '13', oem: baseOem })
|
|
||||||
// Stratégie hybride pour les noms
|
|
||||||
strategies.push({ lang: 'fra+eng', psm: '6', oem: baseOem })
|
|
||||||
strategies.push({ lang: 'fra+eng', psm: '8', oem: baseOem })
|
|
||||||
}
|
|
||||||
|
|
||||||
let best = { text: '', score: -1, meta: null }
|
|
||||||
for (let i = 0; i < strategies.length; i += 1) {
|
|
||||||
const s = strategies[i]
|
|
||||||
const tempOutput = `${tempOutputBase}_${i}`
|
|
||||||
const cmd = `tesseract "${tempInput}" "${tempOutput}" -l ${s.lang} --psm ${s.psm} --oem ${s.oem}`
|
|
||||||
try {
|
|
||||||
execSync(cmd, { stdio: 'pipe' })
|
|
||||||
const t = fs.readFileSync(`${tempOutput}.txt`, 'utf8')
|
|
||||||
const text = t.trim()
|
|
||||||
// Heuristique de score: longueur utile et présence de caractères alphanumériques
|
|
||||||
const alpha = (text.match(/[A-Za-z0-9]/g) || []).length
|
|
||||||
const score = alpha + (text.includes('<<') ? 20 : 0)
|
|
||||||
if (score > best.score) best = { text, score, meta: s }
|
|
||||||
try { fs.unlinkSync(`${tempOutput}.txt`) } catch {}
|
|
||||||
} catch (e) {
|
|
||||||
// Essai suivant
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try { fs.unlinkSync(tempInput) } catch {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: best.text,
|
|
||||||
confidence: best.score > 0 ? 0.85 : 0.6,
|
|
||||||
method: 'tesseract_multi',
|
|
||||||
used: best.meta,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour extraire le texte d'une image avec améliorations CNI
|
|
||||||
async function extractTextFromImageEnhanced(inputPath) {
|
|
||||||
try {
|
|
||||||
console.log(`[ENHANCED_OCR] Début extraction: ${path.basename(inputPath)}`)
|
|
||||||
|
|
||||||
// Vérifier si c'est une CNI
|
|
||||||
const isCNI = await isCNIDocument(inputPath)
|
|
||||||
console.log(`[ENHANCED_OCR] CNI détectée: ${isCNI}`)
|
|
||||||
|
|
||||||
if (isCNI) {
|
|
||||||
return await extractTextFromCNI(inputPath)
|
|
||||||
} else {
|
|
||||||
return await extractTextFromStandardDocument(inputPath)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[ENHANCED_OCR] Erreur extraction:`, error.message)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction spécialisée pour l'extraction de texte des CNI
|
|
||||||
async function extractTextFromCNI(inputPath) {
|
|
||||||
try {
|
|
||||||
console.log(`[CNI_OCR] Traitement CNI: ${path.basename(inputPath)}`)
|
|
||||||
|
|
||||||
// Améliorer le preprocessing pour les CNI
|
|
||||||
const enhancedImage = await enhanceCNIPreprocessing(inputPath)
|
|
||||||
if (!enhancedImage) {
|
|
||||||
throw new Error('Échec du preprocessing CNI')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traitement par zones
|
|
||||||
const cniZones = await processCNIWithZones(inputPath)
|
|
||||||
|
|
||||||
let combinedText = ''
|
|
||||||
let mrzData = null
|
|
||||||
|
|
||||||
// Extraire le texte de l'image améliorée avec stratégies CNI
|
|
||||||
const mainText = await runTesseractOCR(enhancedImage, {
|
|
||||||
language: 'fra',
|
|
||||||
psm: '6', // Mode uniforme de bloc de texte
|
|
||||||
cni: true, // Activer les stratégies spécialisées CNI
|
|
||||||
frenchNames: true, // Activer les stratégies pour noms français
|
|
||||||
})
|
|
||||||
combinedText += mainText.text + '\n'
|
|
||||||
|
|
||||||
// Si on a des zones, traiter chaque zone séparément
|
|
||||||
if (cniZones && cniZones.zones) {
|
|
||||||
for (const [zoneName, zoneImage] of Object.entries(cniZones.zones)) {
|
|
||||||
try {
|
|
||||||
const zoneText = await runTesseractOCR(zoneImage, {
|
|
||||||
language: 'fra',
|
|
||||||
psm: '8', // Mode mot unique
|
|
||||||
cni: true, // Activer les stratégies spécialisées CNI
|
|
||||||
frenchNames: true, // Activer les stratégies pour noms français
|
|
||||||
})
|
|
||||||
combinedText += `[${zoneName.toUpperCase()}] ${zoneText.text}\n`
|
|
||||||
console.log(`[CNI_OCR] Zone ${zoneName}: ${zoneText.text}`)
|
|
||||||
} catch (zoneError) {
|
|
||||||
console.warn(`[CNI_OCR] Erreur zone ${zoneName}:`, zoneError.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traiter la MRZ si disponible
|
|
||||||
if (cniZones && cniZones.mrz) {
|
|
||||||
try {
|
|
||||||
const mrzText = await runTesseractOCR(cniZones.mrz, {
|
|
||||||
language: 'ocrb+eng',
|
|
||||||
psm: '6',
|
|
||||||
mrz: true,
|
|
||||||
})
|
|
||||||
combinedText += `[MRZ] ${mrzText.text}\n`
|
|
||||||
|
|
||||||
// Décoder la MRZ
|
|
||||||
mrzData = decodeMRZ(mrzText.text)
|
|
||||||
if (mrzData) {
|
|
||||||
combinedText += `[MRZ_DECODED] Nom: ${mrzData.surname}, Prénom: ${mrzData.givenNames}, Numéro: ${mrzData.documentNumber}\n`
|
|
||||||
}
|
|
||||||
} catch (mrzError) {
|
|
||||||
console.warn(`[CNI_OCR] Erreur MRZ:`, mrzError.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post-traitement du texte pour corriger les erreurs communes
|
|
||||||
const processedText = postProcessCNIText(combinedText)
|
|
||||||
|
|
||||||
console.log(`[CNI_OCR] Texte final: ${processedText.length} caractères`)
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: processedText,
|
|
||||||
confidence: 0.85, // Confiance élevée pour les CNI traitées
|
|
||||||
method: 'cni_enhanced',
|
|
||||||
mrzData: mrzData,
|
|
||||||
zones: cniZones ? Object.keys(cniZones.zones || {}) : [],
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[CNI_OCR] Erreur traitement CNI:`, error.message)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour l'extraction de texte des documents standards
|
|
||||||
async function extractTextFromStandardDocument(inputPath) {
|
|
||||||
try {
|
|
||||||
console.log(`[STANDARD_OCR] Traitement document standard: ${path.basename(inputPath)}`)
|
|
||||||
|
|
||||||
// Preprocessing standard
|
|
||||||
const image = sharp(inputPath)
|
|
||||||
const metadata = await image.metadata()
|
|
||||||
|
|
||||||
const processedImage = await image
|
|
||||||
.resize({
|
|
||||||
width: Math.min(metadata.width * 2, 2000),
|
|
||||||
height: Math.min(metadata.height * 2, 2000),
|
|
||||||
fit: 'inside',
|
|
||||||
withoutEnlargement: false,
|
|
||||||
})
|
|
||||||
.grayscale()
|
|
||||||
// Sharp attend des bornes entières 1..100 pour lower/upper (percentiles)
|
|
||||||
// Ancien réglage (0.1/0.9) provoquait une erreur. On utilise 10/90.
|
|
||||||
.normalize({ lower: 10, upper: 90 })
|
|
||||||
// Voir commentaire ci-dessus: clamp en 1..100
|
|
||||||
.normalize({ lower: 10, upper: 90 })
|
|
||||||
.sharpen()
|
|
||||||
.png()
|
|
||||||
.toBuffer()
|
|
||||||
|
|
||||||
// OCR standard
|
|
||||||
const result = await runTesseractOCR(processedImage, {
|
|
||||||
language: 'fra',
|
|
||||||
psm: '6',
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: result.text,
|
|
||||||
confidence: result.confidence,
|
|
||||||
method: 'standard_enhanced',
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[STANDARD_OCR] Erreur traitement standard:`, error.message)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonction pour post-traiter le texte des CNI
|
|
||||||
function postProcessCNIText(text) {
|
|
||||||
try {
|
|
||||||
let processedText = text
|
|
||||||
|
|
||||||
// Corrections communes pour les CNI
|
|
||||||
const corrections = [
|
|
||||||
// Corrections de caractères corrompus
|
|
||||||
{ from: /RÉPUBLIQUE FRANCATSEN/g, to: 'RÉPUBLIQUE FRANÇAISE' },
|
|
||||||
{ from: /CARTE NATIONALE DIDENTITE/g, to: "CARTE NATIONALE D'IDENTITÉ" },
|
|
||||||
{ from: /Ne :/g, to: 'N° :' },
|
|
||||||
{ from: /Fe - 0/g, to: 'Féminin' },
|
|
||||||
{ from: /Mele:/g, to: 'Mâle:' },
|
|
||||||
{ from: /IDFRA[A-Z]+CCKKLLLLK[A-Z]*/g, to: 'IDFRA' }, // Nettoyer les caractères parasites après IDFRA
|
|
||||||
|
|
||||||
// Corrections génériques pour les noms corrompus
|
|
||||||
{ from: /([A-Z]{2,})CCKKLLLLK/g, to: '$1' }, // Supprimer les caractères parasites après les noms
|
|
||||||
{ from: /([A-Z]{2,})<+([A-Z]{2,})/g, to: '$1<<$2' }, // Normaliser les séparateurs de noms
|
|
||||||
{ from: /([A-Z]{2,})<<<<([A-Z]{2,})/g, to: '$1<<$2' }, // Réduire les séparateurs multiples
|
|
||||||
{ from: /([A-Z]{2,})<<<<<<([A-Z]{2,})/g, to: '$1<<$2' }, // Réduire les séparateurs multiples
|
|
||||||
{ from: /([A-Z]{2,})<<<<<</g, to: '$1<<' }, // Nettoyer les séparateurs en fin de nom
|
|
||||||
{ from: /([A-Z]{2,})<<<</g, to: '$1<<' }, // Nettoyer les séparateurs en fin de nom
|
|
||||||
|
|
||||||
// Corrections de caractères OCR courants
|
|
||||||
{ from: /0/g, to: 'O' }, // 0 -> O dans les noms
|
|
||||||
{ from: /1/g, to: 'I' }, // 1 -> I dans les noms
|
|
||||||
{ from: /5/g, to: 'S' }, // 5 -> S dans les noms
|
|
||||||
{ from: /8/g, to: 'B' }, // 8 -> B dans les noms
|
|
||||||
{ from: /6/g, to: 'G' }, // 6 -> G dans les noms
|
|
||||||
|
|
||||||
// Corrections génériques pour les erreurs OCR courantes dans les noms
|
|
||||||
{ from: /([A-Z]{2,})0([A-Z]{2,})/g, to: '$1O$2' }, // 0 -> O dans les noms
|
|
||||||
{ from: /([A-Z]{2,})1([A-Z]{2,})/g, to: '$1I$2' }, // 1 -> I dans les noms
|
|
||||||
{ from: /([A-Z]{2,})5([A-Z]{2,})/g, to: '$1S$2' }, // 5 -> S dans les noms
|
|
||||||
{ from: /([A-Z]{2,})8([A-Z]{2,})/g, to: '$1B$2' }, // 8 -> B dans les noms
|
|
||||||
{ from: /([A-Z]{2,})6([A-Z]{2,})/g, to: '$1G$2' }, // 6 -> G dans les noms
|
|
||||||
|
|
||||||
// Nettoyage des caractères parasites
|
|
||||||
{
|
|
||||||
from: /[^\w\sÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ.,;:!?()\-'"]/g,
|
|
||||||
to: ' ',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Normalisation des espaces
|
|
||||||
{ from: /\s+/g, to: ' ' },
|
|
||||||
{ from: /^\s+|\s+$/g, to: '' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// Appliquer les corrections
|
|
||||||
for (const correction of corrections) {
|
|
||||||
processedText = processedText.replace(correction.from, correction.to)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[POST_PROCESS] Texte post-traité: ${processedText.length} caractères`)
|
|
||||||
return processedText
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[POST_PROCESS] Erreur post-traitement:`, error.message)
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
extractTextFromImageEnhanced,
|
|
||||||
extractTextFromCNI,
|
|
||||||
extractTextFromStandardDocument,
|
|
||||||
runTesseractOCR,
|
|
||||||
postProcessCNIText,
|
|
||||||
}
|
|
||||||
@ -1,384 +0,0 @@
|
|||||||
/**
|
|
||||||
* Extraction d'entités métier spécialisées pour les actes notariés
|
|
||||||
* Biens immobiliers, clauses contractuelles, signatures, héritiers, etc.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extraction des biens immobiliers
|
|
||||||
* @param {string} text - Texte à analyser
|
|
||||||
* @returns {Array} Liste des biens immobiliers
|
|
||||||
*/
|
|
||||||
function extractBiensImmobiliers(text) {
|
|
||||||
const biens = []
|
|
||||||
|
|
||||||
// Patterns pour les biens immobiliers
|
|
||||||
const patterns = [
|
|
||||||
// Maison, appartement, terrain
|
|
||||||
/(maison|appartement|terrain|villa|studio|loft|duplex|triplex|pavillon|chalet|château|manoir|hôtel particulier|immeuble|bâtiment|construction|édifice)\s+(?:situé[e]?\s+)?(?:au\s+)?(?:n°\s*)?(\d+[a-z]?)?\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+)/gi,
|
|
||||||
|
|
||||||
// Adresse complète
|
|
||||||
/(?:situé[e]?\s+)?(?:au\s+)?(?:n°\s*)?(\d+[a-z]?)\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+),\s*(\d{5})\s+([^,]+)/gi,
|
|
||||||
|
|
||||||
// Surface et caractéristiques
|
|
||||||
/(?:d'une\s+)?(?:surface\s+de\s+)?(\d+(?:\.\d+)?)\s*(?:m²|m2|mètres?\s+carrés?)/gi,
|
|
||||||
|
|
||||||
// Nombre de pièces
|
|
||||||
/(?:composé[e]?\s+de\s+)?(\d+)\s*(?:pièces?|chambres?|salles?)/gi,
|
|
||||||
|
|
||||||
// Type de bien
|
|
||||||
/(?:un\s+)?(maison|appartement|terrain|villa|studio|loft|duplex|triplex|pavillon|chalet|château|manoir|hôtel particulier|immeuble|bâtiment|construction|édifice)/gi
|
|
||||||
]
|
|
||||||
|
|
||||||
// Extraction des adresses
|
|
||||||
const adresseMatches = text.match(patterns[1]) || []
|
|
||||||
for (const match of adresseMatches) {
|
|
||||||
const parts = match.match(/(\d+[a-z]?)\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+),\s*(\d{5})\s+([^,]+)/i)
|
|
||||||
if (parts) {
|
|
||||||
biens.push({
|
|
||||||
type: 'bien_immobilier',
|
|
||||||
adresse: {
|
|
||||||
numero: parts[1],
|
|
||||||
rue: parts[2].trim(),
|
|
||||||
codePostal: parts[3],
|
|
||||||
ville: parts[4].trim()
|
|
||||||
},
|
|
||||||
surface: null,
|
|
||||||
pieces: null,
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des surfaces
|
|
||||||
const surfaceMatches = text.match(patterns[2]) || []
|
|
||||||
for (const match of surfaceMatches) {
|
|
||||||
const surface = parseFloat(match.match(/(\d+(?:\.\d+)?)/)[1])
|
|
||||||
if (biens.length > 0) {
|
|
||||||
biens[biens.length - 1].surface = surface
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction du nombre de pièces
|
|
||||||
const piecesMatches = text.match(patterns[3]) || []
|
|
||||||
for (const match of piecesMatches) {
|
|
||||||
const pieces = parseInt(match.match(/(\d+)/)[1])
|
|
||||||
if (biens.length > 0) {
|
|
||||||
biens[biens.length - 1].pieces = pieces
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return biens
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extraction des clauses contractuelles
|
|
||||||
* @param {string} text - Texte à analyser
|
|
||||||
* @returns {Array} Liste des clauses
|
|
||||||
*/
|
|
||||||
function extractClauses(text) {
|
|
||||||
const clauses = []
|
|
||||||
|
|
||||||
// Patterns pour les clauses
|
|
||||||
const patterns = [
|
|
||||||
// Clauses de prix
|
|
||||||
/(?:prix|montant|somme)\s+(?:de\s+)?(?:vente\s+)?(?:fixé[e]?\s+à\s+)?(\d+(?:\s+\d+)*(?:\.\d+)?)\s*(?:euros?|€|EUR)/gi,
|
|
||||||
|
|
||||||
// Clauses suspensives
|
|
||||||
/(?:clause\s+)?(?:suspensive|condition)\s+(?:de\s+)?([^.]{10,100})/gi,
|
|
||||||
|
|
||||||
// Clauses de garantie
|
|
||||||
/(?:garantie|garanties?)\s+(?:de\s+)?([^.]{10,100})/gi,
|
|
||||||
|
|
||||||
// Clauses de délai
|
|
||||||
/(?:délai|échéance|terme)\s+(?:de\s+)?(\d+)\s*(?:jours?|mois|années?)/gi,
|
|
||||||
|
|
||||||
// Clauses de résolution
|
|
||||||
/(?:résolution|annulation)\s+(?:du\s+)?(?:contrat|acte)\s+(?:en\s+cas\s+de\s+)?([^.]{10,100})/gi
|
|
||||||
]
|
|
||||||
|
|
||||||
// Extraction des prix
|
|
||||||
const prixMatches = text.match(patterns[0]) || []
|
|
||||||
for (const match of prixMatches) {
|
|
||||||
const prix = match.match(/(\d+(?:\s+\d+)*(?:\.\d+)?)/)[1].replace(/\s+/g, '')
|
|
||||||
clauses.push({
|
|
||||||
type: 'prix',
|
|
||||||
valeur: parseFloat(prix),
|
|
||||||
devise: 'EUR',
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des clauses suspensives
|
|
||||||
const suspensivesMatches = text.match(patterns[1]) || []
|
|
||||||
for (const match of suspensivesMatches) {
|
|
||||||
clauses.push({
|
|
||||||
type: 'clause_suspensive',
|
|
||||||
description: match.trim(),
|
|
||||||
condition: match.replace(/(?:clause\s+)?(?:suspensive|condition)\s+(?:de\s+)?/i, '').trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des garanties
|
|
||||||
const garantiesMatches = text.match(patterns[2]) || []
|
|
||||||
for (const match of garantiesMatches) {
|
|
||||||
clauses.push({
|
|
||||||
type: 'garantie',
|
|
||||||
description: match.trim(),
|
|
||||||
garantie: match.replace(/(?:garantie|garanties?)\s+(?:de\s+)?/i, '').trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return clauses
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extraction des signatures
|
|
||||||
* @param {string} text - Texte à analyser
|
|
||||||
* @returns {Array} Liste des signatures
|
|
||||||
*/
|
|
||||||
function extractSignatures(text) {
|
|
||||||
const signatures = []
|
|
||||||
|
|
||||||
// Patterns pour les signatures
|
|
||||||
const patterns = [
|
|
||||||
// Signature simple
|
|
||||||
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
|
|
||||||
|
|
||||||
// Signature avec date
|
|
||||||
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:le\s+)?(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/gi,
|
|
||||||
|
|
||||||
// Signature avec lieu
|
|
||||||
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:à\s+)?([A-Z][a-z]+)/gi,
|
|
||||||
|
|
||||||
// Signature notariale
|
|
||||||
/(?:notaire|maître)\s+([A-Z][a-z]+\s+[A-Z][a-z]+)/gi
|
|
||||||
]
|
|
||||||
|
|
||||||
// Extraction des signatures simples
|
|
||||||
const signatureMatches = text.match(patterns[0]) || []
|
|
||||||
for (const match of signatureMatches) {
|
|
||||||
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
|
|
||||||
signatures.push({
|
|
||||||
type: 'signature',
|
|
||||||
nom: nom,
|
|
||||||
date: null,
|
|
||||||
lieu: null,
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des signatures avec date
|
|
||||||
const signatureDateMatches = text.match(patterns[1]) || []
|
|
||||||
for (const match of signatureDateMatches) {
|
|
||||||
const parts = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:le\s+)?(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/)
|
|
||||||
if (parts) {
|
|
||||||
signatures.push({
|
|
||||||
type: 'signature',
|
|
||||||
nom: parts[1],
|
|
||||||
date: parts[2],
|
|
||||||
lieu: null,
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des signatures notariales
|
|
||||||
const notaireMatches = text.match(patterns[3]) || []
|
|
||||||
for (const match of notaireMatches) {
|
|
||||||
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
|
|
||||||
signatures.push({
|
|
||||||
type: 'signature_notariale',
|
|
||||||
nom: nom,
|
|
||||||
date: null,
|
|
||||||
lieu: null,
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return signatures
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extraction des héritiers
|
|
||||||
* @param {string} text - Texte à analyser
|
|
||||||
* @returns {Array} Liste des héritiers
|
|
||||||
*/
|
|
||||||
function extractHeritiers(text) {
|
|
||||||
const heritiers = []
|
|
||||||
|
|
||||||
// Patterns pour les héritiers
|
|
||||||
const patterns = [
|
|
||||||
// Héritier simple
|
|
||||||
/(?:héritier|héritière|successeur|successeure|bénéficiaire)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
|
|
||||||
|
|
||||||
// Héritier avec degré de parenté
|
|
||||||
/(?:fils|fille|père|mère|frère|sœur|époux|épouse|mari|femme|conjoint|conjointe|enfant|parent|grand-père|grand-mère|oncle|tante|neveu|nièce|cousin|cousine)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
|
|
||||||
|
|
||||||
// Héritier avec part
|
|
||||||
/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:hérite|bénéficie)\s+(?:de\s+)?(?:la\s+)?(?:part\s+de\s+)?(\d+(?:\/\d+)?|tout|totalité)/gi
|
|
||||||
]
|
|
||||||
|
|
||||||
// Extraction des héritiers simples
|
|
||||||
const heritierMatches = text.match(patterns[0]) || []
|
|
||||||
for (const match of heritierMatches) {
|
|
||||||
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
|
|
||||||
heritiers.push({
|
|
||||||
type: 'heritier',
|
|
||||||
nom: nom,
|
|
||||||
parente: null,
|
|
||||||
part: null,
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des héritiers avec parenté
|
|
||||||
const parenteMatches = text.match(patterns[1]) || []
|
|
||||||
for (const match of parenteMatches) {
|
|
||||||
const parts = match.match(/(fils|fille|père|mère|frère|sœur|époux|épouse|mari|femme|conjoint|conjointe|enfant|parent|grand-père|grand-mère|oncle|tante|neveu|nièce|cousin|cousine)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/i)
|
|
||||||
if (parts) {
|
|
||||||
heritiers.push({
|
|
||||||
type: 'heritier',
|
|
||||||
nom: parts[2],
|
|
||||||
parente: parts[1],
|
|
||||||
part: null,
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return heritiers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extraction des vendeurs et acheteurs
|
|
||||||
* @param {string} text - Texte à analyser
|
|
||||||
* @returns {Object} Vendeurs et acheteurs
|
|
||||||
*/
|
|
||||||
function extractVendeursAcheteurs(text) {
|
|
||||||
const result = {
|
|
||||||
vendeurs: [],
|
|
||||||
acheteurs: []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patterns pour vendeurs et acheteurs
|
|
||||||
const patterns = [
|
|
||||||
// Vendeur
|
|
||||||
/(?:vendeur|vendeuse|vendant|vendant)\s+(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
|
|
||||||
|
|
||||||
// Acheteur
|
|
||||||
/(?:acheteur|acheteuse|achetant|achetant)\s+(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
|
|
||||||
|
|
||||||
// Vente entre
|
|
||||||
/(?:vente\s+)?(?:entre\s+)?(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:et\s+)?(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi
|
|
||||||
]
|
|
||||||
|
|
||||||
// Extraction des vendeurs
|
|
||||||
const vendeurMatches = text.match(patterns[0]) || []
|
|
||||||
for (const match of vendeurMatches) {
|
|
||||||
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
|
|
||||||
result.vendeurs.push({
|
|
||||||
type: 'vendeur',
|
|
||||||
nom: nom,
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des acheteurs
|
|
||||||
const acheteurMatches = text.match(patterns[1]) || []
|
|
||||||
for (const match of acheteurMatches) {
|
|
||||||
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
|
|
||||||
result.acheteurs.push({
|
|
||||||
type: 'acheteur',
|
|
||||||
nom: nom,
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraction des ventes entre
|
|
||||||
const venteMatches = text.match(patterns[2]) || []
|
|
||||||
for (const match of venteMatches) {
|
|
||||||
const parts = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:et\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/)
|
|
||||||
if (parts) {
|
|
||||||
result.vendeurs.push({
|
|
||||||
type: 'vendeur',
|
|
||||||
nom: parts[1],
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
result.acheteurs.push({
|
|
||||||
type: 'acheteur',
|
|
||||||
nom: parts[2],
|
|
||||||
description: match.trim()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Classification du type de document
|
|
||||||
* @param {string} text - Texte à analyser
|
|
||||||
* @returns {string} Type de document
|
|
||||||
*/
|
|
||||||
function classifyDocumentType(text) {
|
|
||||||
const textLower = text.toLowerCase()
|
|
||||||
|
|
||||||
// Types de documents notariés
|
|
||||||
const types = {
|
|
||||||
'acte_vente': ['vente', 'achat', 'acquisition', 'cession', 'transfert'],
|
|
||||||
'acte_succession': ['succession', 'héritage', 'héritier', 'défunt', 'décès'],
|
|
||||||
'acte_donation': ['donation', 'donner', 'donné', 'donateur', 'donataire'],
|
|
||||||
'acte_mariage': ['mariage', 'époux', 'épouse', 'conjoint', 'conjointe'],
|
|
||||||
'acte_divorce': ['divorce', 'séparation', 'liquidation'],
|
|
||||||
'acte_pacs': ['pacs', 'pacte civil', 'solidarité'],
|
|
||||||
'acte_testament': ['testament', 'testateur', 'testatrice', 'legs', 'léguer'],
|
|
||||||
'acte_promesse': ['promesse', 'compromis', 'avant-contrat'],
|
|
||||||
'acte_authentification': ['authentification', 'authentifier', 'certifier'],
|
|
||||||
'acte_pouvoir': ['pouvoir', 'procuration', 'mandat'],
|
|
||||||
'acte_societe': ['société', 'entreprise', 'sarl', 'sas', 'eurl', 'snc']
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calcul des scores
|
|
||||||
const scores = {}
|
|
||||||
for (const [type, keywords] of Object.entries(types)) {
|
|
||||||
scores[type] = keywords.reduce((score, keyword) => {
|
|
||||||
const matches = (textLower.match(new RegExp(keyword, 'g')) || []).length
|
|
||||||
return score + matches
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retourner le type avec le score le plus élevé
|
|
||||||
const bestType = Object.entries(scores).reduce((a, b) => scores[a[0]] > scores[b[0]] ? a : b)
|
|
||||||
|
|
||||||
return bestType[1] > 0 ? bestType[0] : 'document_inconnu'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extraction complète des entités métier
|
|
||||||
* @param {string} text - Texte à analyser
|
|
||||||
* @returns {Object} Toutes les entités extraites
|
|
||||||
*/
|
|
||||||
function extractMetierEntities(text) {
|
|
||||||
return {
|
|
||||||
biensImmobiliers: extractBiensImmobiliers(text),
|
|
||||||
clauses: extractClauses(text),
|
|
||||||
signatures: extractSignatures(text),
|
|
||||||
heritiers: extractHeritiers(text),
|
|
||||||
vendeursAcheteurs: extractVendeursAcheteurs(text),
|
|
||||||
documentType: classifyDocumentType(text),
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
extractBiensImmobiliers,
|
|
||||||
extractClauses,
|
|
||||||
extractSignatures,
|
|
||||||
extractHeritiers,
|
|
||||||
extractVendeursAcheteurs,
|
|
||||||
classifyDocumentType,
|
|
||||||
extractMetierEntities
|
|
||||||
}
|
|
||||||
Binary file not shown.
@ -1,214 +0,0 @@
|
|||||||
/**
|
|
||||||
* Module de préprocessing d'image pour améliorer l'OCR
|
|
||||||
* Optimisé pour les documents d'identité et CNI
|
|
||||||
*/
|
|
||||||
|
|
||||||
const sharp = require('sharp')
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prétraite une image pour améliorer la qualité de l'OCR
|
|
||||||
* @param {string} inputPath - Chemin vers l'image d'entrée
|
|
||||||
* @param {string} outputPath - Chemin vers l'image de sortie (optionnel)
|
|
||||||
* @param {Object} options - Options de préprocessing
|
|
||||||
* @returns {Promise<Buffer>} - Buffer de l'image préprocessée
|
|
||||||
*/
|
|
||||||
async function preprocessImageForOCR(inputPath, outputPath = null, options = {}) {
|
|
||||||
console.log(`[PREPROCESSING] Début du préprocessing de: ${path.basename(inputPath)}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Options par défaut optimisées pour les documents d'identité
|
|
||||||
const defaultOptions = {
|
|
||||||
// Redimensionnement
|
|
||||||
width: 2000, // Largeur cible
|
|
||||||
height: null, // Hauteur automatique (maintient le ratio)
|
|
||||||
|
|
||||||
// Amélioration du contraste
|
|
||||||
contrast: 1.5, // Augmente le contraste
|
|
||||||
brightness: 1.1, // Légère augmentation de la luminosité
|
|
||||||
|
|
||||||
// Filtres
|
|
||||||
sharpen: true, // Amélioration de la netteté
|
|
||||||
denoise: true, // Réduction du bruit
|
|
||||||
|
|
||||||
// Conversion
|
|
||||||
grayscale: true, // Conversion en niveaux de gris
|
|
||||||
threshold: null, // Seuil pour binarisation (optionnel)
|
|
||||||
|
|
||||||
// Format de sortie
|
|
||||||
format: 'png', // Format PNG pour meilleure qualité
|
|
||||||
quality: 100, // Qualité maximale
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = { ...defaultOptions, ...options }
|
|
||||||
|
|
||||||
console.log(`[PREPROCESSING] Configuration:`, {
|
|
||||||
width: config.width,
|
|
||||||
contrast: config.contrast,
|
|
||||||
brightness: config.brightness,
|
|
||||||
grayscale: config.grayscale,
|
|
||||||
sharpen: config.sharpen,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Lecture de l'image
|
|
||||||
let image = sharp(inputPath)
|
|
||||||
|
|
||||||
// Redimensionnement
|
|
||||||
if (config.width || config.height) {
|
|
||||||
image = image.resize(config.width, config.height, {
|
|
||||||
fit: 'inside',
|
|
||||||
withoutEnlargement: false,
|
|
||||||
})
|
|
||||||
console.log(`[PREPROCESSING] Redimensionnement appliqué`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conversion en niveaux de gris
|
|
||||||
if (config.grayscale) {
|
|
||||||
image = image.grayscale()
|
|
||||||
console.log(`[PREPROCESSING] Conversion en niveaux de gris`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Amélioration du contraste et de la luminosité
|
|
||||||
if (config.contrast !== 1 || config.brightness !== 1) {
|
|
||||||
image = image.modulate({
|
|
||||||
brightness: config.brightness,
|
|
||||||
contrast: config.contrast,
|
|
||||||
})
|
|
||||||
console.log(
|
|
||||||
`[PREPROCESSING] Contraste (${config.contrast}) et luminosité (${config.brightness}) appliqués`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Amélioration de la netteté
|
|
||||||
if (config.sharpen) {
|
|
||||||
image = image.sharpen({
|
|
||||||
sigma: 1.0,
|
|
||||||
flat: 1.0,
|
|
||||||
jagged: 2.0,
|
|
||||||
})
|
|
||||||
console.log(`[PREPROCESSING] Amélioration de la netteté appliquée`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Réduction du bruit
|
|
||||||
if (config.denoise) {
|
|
||||||
image = image.median(3)
|
|
||||||
console.log(`[PREPROCESSING] Réduction du bruit appliquée`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binarisation (seuil) si demandée
|
|
||||||
if (config.threshold) {
|
|
||||||
image = image.threshold(config.threshold)
|
|
||||||
console.log(`[PREPROCESSING] Binarisation avec seuil ${config.threshold}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Application des modifications et conversion
|
|
||||||
const processedBuffer = await image.png({ quality: config.quality }).toBuffer()
|
|
||||||
|
|
||||||
console.log(`[PREPROCESSING] Image préprocessée: ${processedBuffer.length} bytes`)
|
|
||||||
|
|
||||||
// Sauvegarde optionnelle
|
|
||||||
if (outputPath) {
|
|
||||||
await fs.promises.writeFile(outputPath, processedBuffer)
|
|
||||||
console.log(`[PREPROCESSING] Image sauvegardée: ${outputPath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedBuffer
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[PREPROCESSING] Erreur lors du préprocessing:`, error.message)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prétraite une image avec plusieurs configurations et retourne la meilleure
|
|
||||||
* @param {string} inputPath - Chemin vers l'image d'entrée
|
|
||||||
* @returns {Promise<Buffer>} - Buffer de la meilleure image préprocessée
|
|
||||||
*/
|
|
||||||
async function preprocessImageMultipleConfigs(inputPath) {
|
|
||||||
console.log(`[PREPROCESSING] Test de plusieurs configurations pour: ${path.basename(inputPath)}`)
|
|
||||||
|
|
||||||
const configs = [
|
|
||||||
{
|
|
||||||
name: 'Standard',
|
|
||||||
options: {
|
|
||||||
width: 2000,
|
|
||||||
contrast: 1.5,
|
|
||||||
brightness: 1.1,
|
|
||||||
grayscale: true,
|
|
||||||
sharpen: true,
|
|
||||||
denoise: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Haute résolution',
|
|
||||||
options: {
|
|
||||||
width: 3000,
|
|
||||||
contrast: 1.8,
|
|
||||||
brightness: 1.2,
|
|
||||||
grayscale: true,
|
|
||||||
sharpen: true,
|
|
||||||
denoise: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Contraste élevé',
|
|
||||||
options: {
|
|
||||||
width: 2000,
|
|
||||||
contrast: 2.0,
|
|
||||||
brightness: 1.0,
|
|
||||||
grayscale: true,
|
|
||||||
sharpen: true,
|
|
||||||
denoise: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Binarisation',
|
|
||||||
options: {
|
|
||||||
width: 2000,
|
|
||||||
contrast: 1.5,
|
|
||||||
brightness: 1.1,
|
|
||||||
grayscale: true,
|
|
||||||
sharpen: true,
|
|
||||||
denoise: true,
|
|
||||||
threshold: 128,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Pour l'instant, on utilise la configuration standard
|
|
||||||
// Dans une version avancée, on pourrait tester toutes les configs
|
|
||||||
const bestConfig = configs[0]
|
|
||||||
console.log(`[PREPROCESSING] Utilisation de la configuration: ${bestConfig.name}`)
|
|
||||||
|
|
||||||
return await preprocessImageForOCR(inputPath, null, bestConfig.options)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyse les métadonnées d'une image
|
|
||||||
* @param {string} imagePath - Chemin vers l'image
|
|
||||||
* @returns {Promise<Object>} - Métadonnées de l'image
|
|
||||||
*/
|
|
||||||
async function analyzeImageMetadata(imagePath) {
|
|
||||||
try {
|
|
||||||
const metadata = await sharp(imagePath).metadata()
|
|
||||||
console.log(`[PREPROCESSING] Métadonnées de ${path.basename(imagePath)}:`, {
|
|
||||||
format: metadata.format,
|
|
||||||
width: metadata.width,
|
|
||||||
height: metadata.height,
|
|
||||||
channels: metadata.channels,
|
|
||||||
density: metadata.density,
|
|
||||||
size: `${(metadata.size / 1024).toFixed(1)} KB`,
|
|
||||||
})
|
|
||||||
return metadata
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[PREPROCESSING] Erreur lors de l'analyse des métadonnées:`, error.message)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
preprocessImageForOCR,
|
|
||||||
preprocessImageMultipleConfigs,
|
|
||||||
analyzeImageMetadata,
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
function loadCsvNames(filePath) {
|
|
||||||
try {
|
|
||||||
const raw = fs.readFileSync(filePath, 'utf8')
|
|
||||||
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)
|
|
||||||
const names = []
|
|
||||||
for (const line of lines) {
|
|
||||||
const parts = line.split(/[;,\t]/).map((p) => p.trim()).filter(Boolean)
|
|
||||||
for (const p of parts) {
|
|
||||||
if (/^[A-Za-zÀ-ÖØ-öø-ÿ'\-\s]{2,}$/.test(p)) names.push(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildNameSets() {
|
|
||||||
const baseDir = path.join(__dirname, 'data', 'names')
|
|
||||||
const firstNames = new Set()
|
|
||||||
const lastNames = new Set()
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(baseDir)) return { firstNames, lastNames }
|
|
||||||
// Prioriser les fichiers unifiés légers (références finales)
|
|
||||||
const preferredOrder = [
|
|
||||||
'firstnames_all.csv',
|
|
||||||
'lastnames_all.csv',
|
|
||||||
]
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(baseDir)
|
|
||||||
.sort((a, b) => preferredOrder.indexOf(a) - preferredOrder.indexOf(b))
|
|
||||||
for (const f of files) {
|
|
||||||
const fp = path.join(baseDir, f)
|
|
||||||
if (!fs.statSync(fp).isFile()) continue
|
|
||||||
// N'utiliser que les deux références finales si présentes
|
|
||||||
const isFirst = /^(firstnames_all\.|first|prenom|given)/i.test(f)
|
|
||||||
const isLast = /^(lastnames_all\.|last|nom|surname|family)/i.test(f)
|
|
||||||
if (!isFirst && !isLast) continue
|
|
||||||
const list = loadCsvNames(fp)
|
|
||||||
for (const n of list) {
|
|
||||||
const norm = n.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
|
||||||
if (isFirst) firstNames.add(norm)
|
|
||||||
if (isLast) lastNames.add(norm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return { firstNames, lastNames }
|
|
||||||
}
|
|
||||||
|
|
||||||
let cache = null
|
|
||||||
function getNameDirectory() {
|
|
||||||
if (!cache) cache = buildNameSets()
|
|
||||||
return cache
|
|
||||||
}
|
|
||||||
|
|
||||||
function nameConfidenceBoost(firstName, lastName) {
|
|
||||||
try {
|
|
||||||
const dir = getNameDirectory()
|
|
||||||
const f = (firstName || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
|
||||||
const l = (lastName || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
|
||||||
let boost = 0
|
|
||||||
if (f && dir.firstNames.has(f)) boost += 0.05
|
|
||||||
if (l && dir.lastNames.has(l)) boost += 0.05
|
|
||||||
return boost
|
|
||||||
} catch {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { getNameDirectory, nameConfidenceBoost }
|
|
||||||
1111
backend/package-lock.json
generated
1111
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "4nk-ia-backend",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"description": "Backend pour le traitement des documents avec OCR et NER",
|
|
||||||
"main": "server.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node server.js",
|
|
||||||
"dev": "node --watch server.js",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"multer": "^2.0.0",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"tesseract.js": "^5.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"ocr",
|
|
||||||
"ner",
|
|
||||||
"document-processing",
|
|
||||||
"express",
|
|
||||||
"tesseract"
|
|
||||||
],
|
|
||||||
"author": "4NK Team",
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
/**
|
|
||||||
* Module de conversion PDF vers images pour l'OCR
|
|
||||||
*/
|
|
||||||
|
|
||||||
const pdf = require('pdf-poppler')
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convertit un PDF en images pour l'OCR
|
|
||||||
* @param {string} pdfPath - Chemin vers le fichier PDF
|
|
||||||
* @param {string} outputDir - Répertoire de sortie (optionnel)
|
|
||||||
* @returns {Promise<Array>} - Tableau des chemins des images générées
|
|
||||||
*/
|
|
||||||
async function convertPdfToImages(pdfPath, outputDir = null) {
|
|
||||||
console.log(`[PDF-CONVERTER] Début de la conversion PDF: ${path.basename(pdfPath)}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Répertoire de sortie par défaut
|
|
||||||
if (!outputDir) {
|
|
||||||
outputDir = path.dirname(pdfPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration de la conversion
|
|
||||||
const options = {
|
|
||||||
format: 'png',
|
|
||||||
out_dir: outputDir,
|
|
||||||
out_prefix: 'page',
|
|
||||||
page: null, // Toutes les pages
|
|
||||||
scale: 2000, // Résolution élevée
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[PDF-CONVERTER] Configuration: Format=PNG, Scale=2000`)
|
|
||||||
|
|
||||||
// Conversion de toutes les pages
|
|
||||||
const results = await pdf.convert(pdfPath, options)
|
|
||||||
|
|
||||||
console.log(`[PDF-CONVERTER] Conversion terminée: ${results.length} page(s) convertie(s)`)
|
|
||||||
|
|
||||||
// Retourner les chemins des images générées
|
|
||||||
const imagePaths = results.map((result, index) => {
|
|
||||||
const imagePath = path.join(outputDir, `page-${index + 1}.png`)
|
|
||||||
console.log(`[PDF-CONVERTER] Page ${index + 1}: ${imagePath}`)
|
|
||||||
return imagePath
|
|
||||||
})
|
|
||||||
|
|
||||||
return imagePaths
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convertit un PDF en une seule image (première page)
|
|
||||||
* @param {string} pdfPath - Chemin vers le fichier PDF
|
|
||||||
* @param {string} outputPath - Chemin de sortie de l'image (optionnel)
|
|
||||||
* @returns {Promise<string>} - Chemin de l'image générée
|
|
||||||
*/
|
|
||||||
async function convertPdfToSingleImage(pdfPath, outputPath = null) {
|
|
||||||
console.log(`[PDF-CONVERTER] Conversion PDF vers image unique: ${path.basename(pdfPath)}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Chemin de sortie par défaut
|
|
||||||
if (!outputPath) {
|
|
||||||
const baseName = path.basename(pdfPath, '.pdf')
|
|
||||||
const dirName = path.dirname(pdfPath)
|
|
||||||
outputPath = path.join(dirName, `${baseName}_converted.png`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration pour une seule page
|
|
||||||
const options = {
|
|
||||||
format: 'png',
|
|
||||||
out_dir: path.dirname(outputPath),
|
|
||||||
out_prefix: path.basename(outputPath, '.png'),
|
|
||||||
page: 1, // Première page seulement
|
|
||||||
scale: 2000,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conversion de la première page seulement
|
|
||||||
const results = await pdf.convert(pdfPath, options)
|
|
||||||
|
|
||||||
console.log(`[PDF-CONVERTER] Image générée: ${outputPath}`)
|
|
||||||
|
|
||||||
return outputPath
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[PDF-CONVERTER] Erreur lors de la conversion:`, error.message)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nettoie les fichiers temporaires générés
|
|
||||||
* @param {Array} filePaths - Chemins des fichiers à supprimer
|
|
||||||
*/
|
|
||||||
async function cleanupTempFiles(filePaths) {
|
|
||||||
console.log(`[PDF-CONVERTER] Nettoyage de ${filePaths.length} fichier(s) temporaire(s)`)
|
|
||||||
|
|
||||||
for (const filePath of filePaths) {
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
await fs.promises.unlink(filePath)
|
|
||||||
console.log(`[PDF-CONVERTER] Fichier supprimé: ${path.basename(filePath)}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[PDF-CONVERTER] Erreur lors de la suppression de ${filePath}: ${error.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
convertPdfToImages,
|
|
||||||
convertPdfToSingleImage,
|
|
||||||
cleanupTempFiles,
|
|
||||||
}
|
|
||||||
2865
backend/server.js
2865
backend/server.js
File diff suppressed because it is too large
Load Diff
@ -1,78 +0,0 @@
|
|||||||
{
|
|
||||||
"document": {
|
|
||||||
"id": "doc-1757978816308",
|
|
||||||
"fileName": "IMG_20250902_162210.jpg",
|
|
||||||
"fileSize": 980755,
|
|
||||||
"mimeType": "image/jpeg",
|
|
||||||
"uploadTimestamp": "2025-09-15T23:26:56.308Z"
|
|
||||||
},
|
|
||||||
"classification": {
|
|
||||||
"documentType": "Document",
|
|
||||||
"confidence": 0.6,
|
|
||||||
"subType": "Document",
|
|
||||||
"language": "fr",
|
|
||||||
"pageCount": 1
|
|
||||||
},
|
|
||||||
"extraction": {
|
|
||||||
"text": {
|
|
||||||
"raw": "4 . al -e ge :\na 4 mT. a ES Se Gaga EES hoe i -\na\nLo ee\nBaie: JERE REEMISEEE -\n1 ss : :-\n1) Carevalablejusques: 17.01.2033 À\n3 déévét: 1801 2018 : Ë 3\n\n Ban a / J\n\nPoa mec penses . -\n",
|
|
||||||
"processed": "4 . al -e ge :\na 4 mT. a ES Se Gaga EES hoe i -\na\nLo ee\nBaie: JERE REEMISEEE -\nl ss : :-\nl) Carevalablejusques: l7.ol.2oee À\ne déévét: l8ol 2ol8 : Ë e\n\n Ban a / J\n\nPoa mec penses . -\n",
|
|
||||||
"wordCount": 48,
|
|
||||||
"characterCount": 338,
|
|
||||||
"confidence": 0.28
|
|
||||||
},
|
|
||||||
"entities": {
|
|
||||||
"persons": [
|
|
||||||
{
|
|
||||||
"id": "identity-0",
|
|
||||||
"type": "person",
|
|
||||||
"firstName": "Se",
|
|
||||||
"lastName": "Gaga",
|
|
||||||
"role": null,
|
|
||||||
"email": null,
|
|
||||||
"phone": null,
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"companies": [],
|
|
||||||
"addresses": [],
|
|
||||||
"financial": {
|
|
||||||
"amounts": [],
|
|
||||||
"totals": {},
|
|
||||||
"payment": {}
|
|
||||||
},
|
|
||||||
"dates": [],
|
|
||||||
"contractual": {
|
|
||||||
"clauses": [],
|
|
||||||
"signatures": []
|
|
||||||
},
|
|
||||||
"references": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"processing": {
|
|
||||||
"engine": "4NK_IA_Backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"processingTime": "4933ms",
|
|
||||||
"ocrEngine": "tesseract.js",
|
|
||||||
"nerEngine": "rule-based",
|
|
||||||
"preprocessing": {
|
|
||||||
"applied": true,
|
|
||||||
"reason": "Image preprocessing applied"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"quality": {
|
|
||||||
"globalConfidence": 0.6,
|
|
||||||
"textExtractionConfidence": 0.28,
|
|
||||||
"entityExtractionConfidence": 0.9,
|
|
||||||
"classificationConfidence": 0.6
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"success": true,
|
|
||||||
"errors": [],
|
|
||||||
"warnings": ["Aucune signature détectée"],
|
|
||||||
"timestamp": "2025-09-15T23:26:56.308Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
178
coverage/App.tsx.html
Normal file
178
coverage/App.tsx.html
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for App.tsx</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="stylesheet" href="prettify.css" />
|
||||||
|
<link rel="stylesheet" href="base.css" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style type='text/css'>
|
||||||
|
.coverage-summary .sorter {
|
||||||
|
background-image: url(sort-arrow-sprite.png);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="index.html">All files</a> App.tsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>0/27</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>0/1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>0/1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>0/27</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<p class="quiet">
|
||||||
|
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line low'></div>
|
||||||
|
<pre><table class="coverage">
|
||||||
|
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
||||||
|
<a name='L2'></a><a href='#L2'>2</a>
|
||||||
|
<a name='L3'></a><a href='#L3'>3</a>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a></td><td class="line-coverage quiet"><span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js"><span class="cstat-no" title="statement not covered" >import { useState } from 'react'<span class="fstat-no" title="function not covered" ><span class="branch-0 cbranch-no" title="branch not covered" ></span></span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" >import reactLogo from './assets/react.svg'</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" >import viteLogo from '/vite.svg'</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" >import './App.css'</span>
|
||||||
|
|
||||||
|
<span class="cstat-no" title="statement not covered" >function App() {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > const [count, setCount] = useState(0)</span>
|
||||||
|
|
||||||
|
<span class="cstat-no" title="statement not covered" > return (</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <div></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <a href="https://vite.dev" target="_blank"></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <img src={viteLogo} className="logo" alt="Vite logo" /></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > </a></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <a href="https://react.dev" target="_blank"></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <img src={reactLogo} className="logo react" alt="React logo" /></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > </a></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > </div></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <h1>Vite + React</h1></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <div className="card"></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <button onClick={() => setCount((count) => count + 1)}>count is {count}</button></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <p></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > Edit <code>src/App.tsx</code> and save to test HMR</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > </p></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > </div></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <p className="read-the-docs">Click on the Vite and React logos to learn more</p></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > </></span>
|
||||||
|
)
|
||||||
|
<span class="cstat-no" title="statement not covered" >}</span>
|
||||||
|
|
||||||
|
<span class="cstat-no" title="statement not covered" >export default App</span>
|
||||||
|
</pre></td></tr></table></pre>
|
||||||
|
|
||||||
|
<div class='push'></div><!-- for sticky footer -->
|
||||||
|
</div><!-- /wrapper -->
|
||||||
|
<div class='footer quiet pad2 space-top1 center small'>
|
||||||
|
Code coverage generated by
|
||||||
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||||
|
at 2025-09-10T15:15:07.789Z
|
||||||
|
</div>
|
||||||
|
<script src="prettify.js"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = function () {
|
||||||
|
prettyPrint();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="sorter.js"></script>
|
||||||
|
<script src="block-navigation.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
224
coverage/base.css
Normal file
224
coverage/base.css
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
body, html {
|
||||||
|
margin:0; padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Helvetica Neue, Helvetica, Arial;
|
||||||
|
font-size: 14px;
|
||||||
|
color:#333;
|
||||||
|
}
|
||||||
|
.small { font-size: 12px; }
|
||||||
|
*, *:after, *:before {
|
||||||
|
-webkit-box-sizing:border-box;
|
||||||
|
-moz-box-sizing:border-box;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
|
h1 { font-size: 20px; margin: 0;}
|
||||||
|
h2 { font-size: 14px; }
|
||||||
|
pre {
|
||||||
|
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-moz-tab-size: 2;
|
||||||
|
-o-tab-size: 2;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
a { color:#0074D9; text-decoration:none; }
|
||||||
|
a:hover { text-decoration:underline; }
|
||||||
|
.strong { font-weight: bold; }
|
||||||
|
.space-top1 { padding: 10px 0 0 0; }
|
||||||
|
.pad2y { padding: 20px 0; }
|
||||||
|
.pad1y { padding: 10px 0; }
|
||||||
|
.pad2x { padding: 0 20px; }
|
||||||
|
.pad2 { padding: 20px; }
|
||||||
|
.pad1 { padding: 10px; }
|
||||||
|
.space-left2 { padding-left:55px; }
|
||||||
|
.space-right2 { padding-right:20px; }
|
||||||
|
.center { text-align:center; }
|
||||||
|
.clearfix { display:block; }
|
||||||
|
.clearfix:after {
|
||||||
|
content:'';
|
||||||
|
display:block;
|
||||||
|
height:0;
|
||||||
|
clear:both;
|
||||||
|
visibility:hidden;
|
||||||
|
}
|
||||||
|
.fl { float: left; }
|
||||||
|
@media only screen and (max-width:640px) {
|
||||||
|
.col3 { width:100%; max-width:100%; }
|
||||||
|
.hide-mobile { display:none!important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiet {
|
||||||
|
color: #7f7f7f;
|
||||||
|
color: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.quiet a { opacity: 0.7; }
|
||||||
|
|
||||||
|
.fraction {
|
||||||
|
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #555;
|
||||||
|
background: #E8E8E8;
|
||||||
|
padding: 4px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.path a:link, div.path a:visited { color: #333; }
|
||||||
|
table.coverage {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.coverage td {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
table.coverage td.line-count {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 5px 0 20px;
|
||||||
|
}
|
||||||
|
table.coverage td.line-coverage {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 10px;
|
||||||
|
min-width:20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.coverage td span.cline-any {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.missing-if-branch {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: #333;
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-if-branch {
|
||||||
|
display: none;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: #ccc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.missing-if-branch .typ, .skip-if-branch .typ {
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
.coverage-summary {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
||||||
|
.keyline-all { border: 1px solid #ddd; }
|
||||||
|
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
||||||
|
.coverage-summary tbody { border: 1px solid #bbb; }
|
||||||
|
.coverage-summary td { border-right: 1px solid #bbb; }
|
||||||
|
.coverage-summary td:last-child { border-right: none; }
|
||||||
|
.coverage-summary th {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.coverage-summary th.file { border-right: none !important; }
|
||||||
|
.coverage-summary th.pct { }
|
||||||
|
.coverage-summary th.pic,
|
||||||
|
.coverage-summary th.abs,
|
||||||
|
.coverage-summary td.pct,
|
||||||
|
.coverage-summary td.abs { text-align: right; }
|
||||||
|
.coverage-summary td.file { white-space: nowrap; }
|
||||||
|
.coverage-summary td.pic { min-width: 120px !important; }
|
||||||
|
.coverage-summary tfoot td { }
|
||||||
|
|
||||||
|
.coverage-summary .sorter {
|
||||||
|
height: 10px;
|
||||||
|
width: 7px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
||||||
|
}
|
||||||
|
.coverage-summary .sorted .sorter {
|
||||||
|
background-position: 0 -20px;
|
||||||
|
}
|
||||||
|
.coverage-summary .sorted-desc .sorter {
|
||||||
|
background-position: 0 -10px;
|
||||||
|
}
|
||||||
|
.status-line { height: 10px; }
|
||||||
|
/* yellow */
|
||||||
|
.cbranch-no { background: yellow !important; color: #111; }
|
||||||
|
/* dark red */
|
||||||
|
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
||||||
|
.low .chart { border:1px solid #C21F39 }
|
||||||
|
.highlighted,
|
||||||
|
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
||||||
|
background: #C21F39 !important;
|
||||||
|
}
|
||||||
|
/* medium red */
|
||||||
|
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
||||||
|
/* light red */
|
||||||
|
.low, .cline-no { background:#FCE1E5 }
|
||||||
|
/* light green */
|
||||||
|
.high, .cline-yes { background:rgb(230,245,208) }
|
||||||
|
/* medium green */
|
||||||
|
.cstat-yes { background:rgb(161,215,106) }
|
||||||
|
/* dark green */
|
||||||
|
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
||||||
|
.high .chart { border:1px solid rgb(77,146,33) }
|
||||||
|
/* dark yellow (gold) */
|
||||||
|
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
||||||
|
.medium .chart { border:1px solid #f9cd0b; }
|
||||||
|
/* light yellow */
|
||||||
|
.medium { background: #fff4c2; }
|
||||||
|
|
||||||
|
.cstat-skip { background: #ddd; color: #111; }
|
||||||
|
.fstat-skip { background: #ddd; color: #111 !important; }
|
||||||
|
.cbranch-skip { background: #ddd !important; color: #111; }
|
||||||
|
|
||||||
|
span.cline-neutral { background: #eaeaea; }
|
||||||
|
|
||||||
|
.coverage-summary td.empty {
|
||||||
|
opacity: .5;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-fill, .cover-empty {
|
||||||
|
display:inline-block;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
.chart {
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
.cover-empty {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.cover-full {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
pre.prettyprint {
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.com { color: #999 !important; }
|
||||||
|
.ignore-none { color: #999; font-weight: normal; }
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto -48px;
|
||||||
|
}
|
||||||
|
.footer, .push {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
87
coverage/block-navigation.js
Normal file
87
coverage/block-navigation.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
var jumpToCode = (function init() {
|
||||||
|
// Classes of code we would like to highlight in the file view
|
||||||
|
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
|
||||||
|
|
||||||
|
// Elements to highlight in the file listing view
|
||||||
|
var fileListingElements = ['td.pct.low'];
|
||||||
|
|
||||||
|
// We don't want to select elements that are direct descendants of another match
|
||||||
|
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
||||||
|
|
||||||
|
// Selector that finds elements on the page to which we can jump
|
||||||
|
var selector =
|
||||||
|
fileListingElements.join(', ') +
|
||||||
|
', ' +
|
||||||
|
notSelector +
|
||||||
|
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
||||||
|
|
||||||
|
// The NodeList of matching elements
|
||||||
|
var missingCoverageElements = document.querySelectorAll(selector);
|
||||||
|
|
||||||
|
var currentIndex;
|
||||||
|
|
||||||
|
function toggleClass(index) {
|
||||||
|
missingCoverageElements
|
||||||
|
.item(currentIndex)
|
||||||
|
.classList.remove('highlighted');
|
||||||
|
missingCoverageElements.item(index).classList.add('highlighted');
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCurrent(index) {
|
||||||
|
toggleClass(index);
|
||||||
|
currentIndex = index;
|
||||||
|
missingCoverageElements.item(index).scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPrevious() {
|
||||||
|
var nextIndex = 0;
|
||||||
|
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
||||||
|
nextIndex = missingCoverageElements.length - 1;
|
||||||
|
} else if (missingCoverageElements.length > 1) {
|
||||||
|
nextIndex = currentIndex - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCurrent(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNext() {
|
||||||
|
var nextIndex = 0;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof currentIndex === 'number' &&
|
||||||
|
currentIndex < missingCoverageElements.length - 1
|
||||||
|
) {
|
||||||
|
nextIndex = currentIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCurrent(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function jump(event) {
|
||||||
|
if (
|
||||||
|
document.getElementById('fileSearch') === document.activeElement &&
|
||||||
|
document.activeElement != null
|
||||||
|
) {
|
||||||
|
// if we're currently focused on the search input, we don't want to navigate
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.which) {
|
||||||
|
case 78: // n
|
||||||
|
case 74: // j
|
||||||
|
goToNext();
|
||||||
|
break;
|
||||||
|
case 66: // b
|
||||||
|
case 75: // k
|
||||||
|
case 80: // p
|
||||||
|
goToPrevious();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
window.addEventListener('keydown', jumpToCode);
|
||||||
48
coverage/clover.xml
Normal file
48
coverage/clover.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<coverage generated="1757517307798" clover="3.2.0">
|
||||||
|
<project timestamp="1757517307798" name="All files">
|
||||||
|
<metrics statements="36" coveredstatements="0" conditionals="2" coveredconditionals="0" methods="2" coveredmethods="0" elements="40" coveredelements="0" complexity="0" loc="36" ncloc="36" packages="1" files="2" classes="2"/>
|
||||||
|
<file name="App.tsx" path="/home/desk/code/4NK_IA_front/src/App.tsx">
|
||||||
|
<metrics statements="27" coveredstatements="0" conditionals="1" coveredconditionals="0" methods="1" coveredmethods="0"/>
|
||||||
|
<line num="1" count="0" type="cond" truecount="0" falsecount="1"/>
|
||||||
|
<line num="2" count="0" type="stmt"/>
|
||||||
|
<line num="3" count="0" type="stmt"/>
|
||||||
|
<line num="4" count="0" type="stmt"/>
|
||||||
|
<line num="6" count="0" type="stmt"/>
|
||||||
|
<line num="7" count="0" type="stmt"/>
|
||||||
|
<line num="9" count="0" type="stmt"/>
|
||||||
|
<line num="10" count="0" type="stmt"/>
|
||||||
|
<line num="11" count="0" type="stmt"/>
|
||||||
|
<line num="12" count="0" type="stmt"/>
|
||||||
|
<line num="13" count="0" type="stmt"/>
|
||||||
|
<line num="14" count="0" type="stmt"/>
|
||||||
|
<line num="15" count="0" type="stmt"/>
|
||||||
|
<line num="16" count="0" type="stmt"/>
|
||||||
|
<line num="17" count="0" type="stmt"/>
|
||||||
|
<line num="18" count="0" type="stmt"/>
|
||||||
|
<line num="19" count="0" type="stmt"/>
|
||||||
|
<line num="20" count="0" type="stmt"/>
|
||||||
|
<line num="21" count="0" type="stmt"/>
|
||||||
|
<line num="22" count="0" type="stmt"/>
|
||||||
|
<line num="23" count="0" type="stmt"/>
|
||||||
|
<line num="24" count="0" type="stmt"/>
|
||||||
|
<line num="25" count="0" type="stmt"/>
|
||||||
|
<line num="26" count="0" type="stmt"/>
|
||||||
|
<line num="27" count="0" type="stmt"/>
|
||||||
|
<line num="29" count="0" type="stmt"/>
|
||||||
|
<line num="31" count="0" type="stmt"/>
|
||||||
|
</file>
|
||||||
|
<file name="main.tsx" path="/home/desk/code/4NK_IA_front/src/main.tsx">
|
||||||
|
<metrics statements="9" coveredstatements="0" conditionals="1" coveredconditionals="0" methods="1" coveredmethods="0"/>
|
||||||
|
<line num="1" count="0" type="cond" truecount="0" falsecount="1"/>
|
||||||
|
<line num="2" count="0" type="stmt"/>
|
||||||
|
<line num="3" count="0" type="stmt"/>
|
||||||
|
<line num="4" count="0" type="stmt"/>
|
||||||
|
<line num="6" count="0" type="stmt"/>
|
||||||
|
<line num="7" count="0" type="stmt"/>
|
||||||
|
<line num="8" count="0" type="stmt"/>
|
||||||
|
<line num="9" count="0" type="stmt"/>
|
||||||
|
<line num="10" count="0" type="stmt"/>
|
||||||
|
</file>
|
||||||
|
</project>
|
||||||
|
</coverage>
|
||||||
3
coverage/coverage-final.json
Normal file
3
coverage/coverage-final.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{"/home/desk/code/4NK_IA_front/src/App.tsx": {"path":"/home/desk/code/4NK_IA_front/src/App.tsx","all":true,"statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":32}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":42}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":32}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":18}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":16}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":39}},"8":{"start":{"line":9,"column":0},"end":{"line":9,"column":10}},"9":{"start":{"line":10,"column":0},"end":{"line":10,"column":6}},"10":{"start":{"line":11,"column":0},"end":{"line":11,"column":11}},"11":{"start":{"line":12,"column":0},"end":{"line":12,"column":51}},"12":{"start":{"line":13,"column":0},"end":{"line":13,"column":65}},"13":{"start":{"line":14,"column":0},"end":{"line":14,"column":12}},"14":{"start":{"line":15,"column":0},"end":{"line":15,"column":52}},"15":{"start":{"line":16,"column":0},"end":{"line":16,"column":73}},"16":{"start":{"line":17,"column":0},"end":{"line":17,"column":12}},"17":{"start":{"line":18,"column":0},"end":{"line":18,"column":12}},"18":{"start":{"line":19,"column":0},"end":{"line":19,"column":27}},"19":{"start":{"line":20,"column":0},"end":{"line":20,"column":28}},"20":{"start":{"line":21,"column":0},"end":{"line":21,"column":88}},"21":{"start":{"line":22,"column":0},"end":{"line":22,"column":11}},"22":{"start":{"line":23,"column":0},"end":{"line":23,"column":60}},"23":{"start":{"line":24,"column":0},"end":{"line":24,"column":12}},"24":{"start":{"line":25,"column":0},"end":{"line":25,"column":12}},"25":{"start":{"line":26,"column":0},"end":{"line":26,"column":86}},"26":{"start":{"line":27,"column":0},"end":{"line":27,"column":7}},"28":{"start":{"line":29,"column":0},"end":{"line":29,"column":1}},"30":{"start":{"line":31,"column":0},"end":{"line":31,"column":18}}},"s":{"0":0,"1":0,"2":0,"3":0,"5":0,"6":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"28":0,"30":0},"branchMap":{"0":{"type":"branch","line":1,"loc":{"start":{"line":1,"column":866},"end":{"line":31,"column":18}},"locations":[{"start":{"line":1,"column":866},"end":{"line":31,"column":18}}]}},"b":{"0":[0]},"fnMap":{"0":{"name":"(empty-report)","decl":{"start":{"line":1,"column":866},"end":{"line":31,"column":18}},"loc":{"start":{"line":1,"column":866},"end":{"line":31,"column":18}},"line":1}},"f":{"0":0}}
|
||||||
|
,"/home/desk/code/4NK_IA_front/src/main.tsx": {"path":"/home/desk/code/4NK_IA_front/src/main.tsx","all":true,"statementMap":{"0":{"start":{"line":1,"column":0},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":0},"end":{"line":2,"column":45}},"2":{"start":{"line":3,"column":0},"end":{"line":3,"column":20}},"3":{"start":{"line":4,"column":0},"end":{"line":4,"column":27}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":52}},"6":{"start":{"line":7,"column":0},"end":{"line":7,"column":14}},"7":{"start":{"line":8,"column":0},"end":{"line":8,"column":11}},"8":{"start":{"line":9,"column":0},"end":{"line":9,"column":16}},"9":{"start":{"line":10,"column":0},"end":{"line":10,"column":1}}},"s":{"0":0,"1":0,"2":0,"3":0,"5":0,"6":0,"7":0,"8":0,"9":0},"branchMap":{"0":{"type":"branch","line":1,"loc":{"start":{"line":1,"column":203},"end":{"line":10,"column":-148}},"locations":[{"start":{"line":1,"column":203},"end":{"line":10,"column":-148}}]}},"b":{"0":[0]},"fnMap":{"0":{"name":"(empty-report)","decl":{"start":{"line":1,"column":203},"end":{"line":10,"column":-148}},"loc":{"start":{"line":1,"column":203},"end":{"line":10,"column":-148}},"line":1}},"f":{"0":0}}
|
||||||
|
}
|
||||||
BIN
coverage/favicon.png
Normal file
BIN
coverage/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 445 B |
131
coverage/index.html
Normal file
131
coverage/index.html
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for All files</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="stylesheet" href="prettify.css" />
|
||||||
|
<link rel="stylesheet" href="base.css" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style type='text/css'>
|
||||||
|
.coverage-summary .sorter {
|
||||||
|
background-image: url(sort-arrow-sprite.png);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1>All files</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>0/36</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>0/2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>0/2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>0/36</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<p class="quiet">
|
||||||
|
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line low'></div>
|
||||||
|
<div class="pad1">
|
||||||
|
<table class="coverage-summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||||
|
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||||
|
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||||
|
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||||
|
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||||
|
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||||
|
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="file low" data-value="App.tsx"><a href="App.tsx.html">App.tsx</a></td>
|
||||||
|
<td data-value="0" class="pic low">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="27" class="abs low">0/27</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="1" class="abs low">0/1</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="1" class="abs low">0/1</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="27" class="abs low">0/27</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file low" data-value="main.tsx"><a href="main.tsx.html">main.tsx</a></td>
|
||||||
|
<td data-value="0" class="pic low">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="9" class="abs low">0/9</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="1" class="abs low">0/1</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="1" class="abs low">0/1</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="9" class="abs low">0/9</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class='push'></div><!-- for sticky footer -->
|
||||||
|
</div><!-- /wrapper -->
|
||||||
|
<div class='footer quiet pad2 space-top1 center small'>
|
||||||
|
Code coverage generated by
|
||||||
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||||
|
at 2025-09-10T15:15:07.789Z
|
||||||
|
</div>
|
||||||
|
<script src="prettify.js"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = function () {
|
||||||
|
prettyPrint();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="sorter.js"></script>
|
||||||
|
<script src="block-navigation.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
115
coverage/main.tsx.html
Normal file
115
coverage/main.tsx.html
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for main.tsx</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="stylesheet" href="prettify.css" />
|
||||||
|
<link rel="stylesheet" href="base.css" />
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style type='text/css'>
|
||||||
|
.coverage-summary .sorter {
|
||||||
|
background-image: url(sort-arrow-sprite.png);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="index.html">All files</a> main.tsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>0/9</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>0/1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>0/1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">0% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>0/9</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<p class="quiet">
|
||||||
|
Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line low'></div>
|
||||||
|
<pre><table class="coverage">
|
||||||
|
<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
|
||||||
|
<a name='L2'></a><a href='#L2'>2</a>
|
||||||
|
<a name='L3'></a><a href='#L3'>3</a>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a></td><td class="line-coverage quiet"><span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js"><span class="cstat-no" title="statement not covered" >import { StrictMode } from 'react'<span class="fstat-no" title="function not covered" ><span class="branch-0 cbranch-no" title="branch not covered" ></span></span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" >import { createRoot } from 'react-dom/client'</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" >import './index.css'</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" >import App from './App.tsx'</span>
|
||||||
|
|
||||||
|
<span class="cstat-no" title="statement not covered" >createRoot(document.getElementById('root')!).render(</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <StrictMode></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > <App /></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > </StrictMode>,</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" >)</span>
|
||||||
|
</pre></td></tr></table></pre>
|
||||||
|
|
||||||
|
<div class='push'></div><!-- for sticky footer -->
|
||||||
|
</div><!-- /wrapper -->
|
||||||
|
<div class='footer quiet pad2 space-top1 center small'>
|
||||||
|
Code coverage generated by
|
||||||
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||||
|
at 2025-09-10T15:15:07.789Z
|
||||||
|
</div>
|
||||||
|
<script src="prettify.js"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = function () {
|
||||||
|
prettyPrint();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="sorter.js"></script>
|
||||||
|
<script src="block-navigation.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
1
coverage/prettify.css
Normal file
1
coverage/prettify.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
|
||||||
2
coverage/prettify.js
Normal file
2
coverage/prettify.js
Normal file
File diff suppressed because one or more lines are too long
BIN
coverage/sort-arrow-sprite.png
Normal file
BIN
coverage/sort-arrow-sprite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 B |
210
coverage/sorter.js
Normal file
210
coverage/sorter.js
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
var addSorting = (function() {
|
||||||
|
'use strict';
|
||||||
|
var cols,
|
||||||
|
currentSort = {
|
||||||
|
index: 0,
|
||||||
|
desc: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// returns the summary table element
|
||||||
|
function getTable() {
|
||||||
|
return document.querySelector('.coverage-summary');
|
||||||
|
}
|
||||||
|
// returns the thead element of the summary table
|
||||||
|
function getTableHeader() {
|
||||||
|
return getTable().querySelector('thead tr');
|
||||||
|
}
|
||||||
|
// returns the tbody element of the summary table
|
||||||
|
function getTableBody() {
|
||||||
|
return getTable().querySelector('tbody');
|
||||||
|
}
|
||||||
|
// returns the th element for nth column
|
||||||
|
function getNthColumn(n) {
|
||||||
|
return getTableHeader().querySelectorAll('th')[n];
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilterInput() {
|
||||||
|
const searchValue = document.getElementById('fileSearch').value;
|
||||||
|
const rows = document.getElementsByTagName('tbody')[0].children;
|
||||||
|
|
||||||
|
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
|
||||||
|
// it will be treated as a plain text search
|
||||||
|
let searchRegex;
|
||||||
|
try {
|
||||||
|
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
|
||||||
|
} catch (error) {
|
||||||
|
searchRegex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const row = rows[i];
|
||||||
|
let isMatch = false;
|
||||||
|
|
||||||
|
if (searchRegex) {
|
||||||
|
// If a valid regex was created, use it for matching
|
||||||
|
isMatch = searchRegex.test(row.textContent);
|
||||||
|
} else {
|
||||||
|
// Otherwise, fall back to the original plain text search
|
||||||
|
isMatch = row.textContent
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchValue.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
row.style.display = isMatch ? '' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads the search box
|
||||||
|
function addSearchBox() {
|
||||||
|
var template = document.getElementById('filterTemplate');
|
||||||
|
var templateClone = template.content.cloneNode(true);
|
||||||
|
templateClone.getElementById('fileSearch').oninput = onFilterInput;
|
||||||
|
template.parentElement.appendChild(templateClone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads all columns
|
||||||
|
function loadColumns() {
|
||||||
|
var colNodes = getTableHeader().querySelectorAll('th'),
|
||||||
|
colNode,
|
||||||
|
cols = [],
|
||||||
|
col,
|
||||||
|
i;
|
||||||
|
|
||||||
|
for (i = 0; i < colNodes.length; i += 1) {
|
||||||
|
colNode = colNodes[i];
|
||||||
|
col = {
|
||||||
|
key: colNode.getAttribute('data-col'),
|
||||||
|
sortable: !colNode.getAttribute('data-nosort'),
|
||||||
|
type: colNode.getAttribute('data-type') || 'string'
|
||||||
|
};
|
||||||
|
cols.push(col);
|
||||||
|
if (col.sortable) {
|
||||||
|
col.defaultDescSort = col.type === 'number';
|
||||||
|
colNode.innerHTML =
|
||||||
|
colNode.innerHTML + '<span class="sorter"></span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
// attaches a data attribute to every tr element with an object
|
||||||
|
// of data values keyed by column name
|
||||||
|
function loadRowData(tableRow) {
|
||||||
|
var tableCols = tableRow.querySelectorAll('td'),
|
||||||
|
colNode,
|
||||||
|
col,
|
||||||
|
data = {},
|
||||||
|
i,
|
||||||
|
val;
|
||||||
|
for (i = 0; i < tableCols.length; i += 1) {
|
||||||
|
colNode = tableCols[i];
|
||||||
|
col = cols[i];
|
||||||
|
val = colNode.getAttribute('data-value');
|
||||||
|
if (col.type === 'number') {
|
||||||
|
val = Number(val);
|
||||||
|
}
|
||||||
|
data[col.key] = val;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
// loads all row data
|
||||||
|
function loadData() {
|
||||||
|
var rows = getTableBody().querySelectorAll('tr'),
|
||||||
|
i;
|
||||||
|
|
||||||
|
for (i = 0; i < rows.length; i += 1) {
|
||||||
|
rows[i].data = loadRowData(rows[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sorts the table using the data for the ith column
|
||||||
|
function sortByIndex(index, desc) {
|
||||||
|
var key = cols[index].key,
|
||||||
|
sorter = function(a, b) {
|
||||||
|
a = a.data[key];
|
||||||
|
b = b.data[key];
|
||||||
|
return a < b ? -1 : a > b ? 1 : 0;
|
||||||
|
},
|
||||||
|
finalSorter = sorter,
|
||||||
|
tableBody = document.querySelector('.coverage-summary tbody'),
|
||||||
|
rowNodes = tableBody.querySelectorAll('tr'),
|
||||||
|
rows = [],
|
||||||
|
i;
|
||||||
|
|
||||||
|
if (desc) {
|
||||||
|
finalSorter = function(a, b) {
|
||||||
|
return -1 * sorter(a, b);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < rowNodes.length; i += 1) {
|
||||||
|
rows.push(rowNodes[i]);
|
||||||
|
tableBody.removeChild(rowNodes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.sort(finalSorter);
|
||||||
|
|
||||||
|
for (i = 0; i < rows.length; i += 1) {
|
||||||
|
tableBody.appendChild(rows[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// removes sort indicators for current column being sorted
|
||||||
|
function removeSortIndicators() {
|
||||||
|
var col = getNthColumn(currentSort.index),
|
||||||
|
cls = col.className;
|
||||||
|
|
||||||
|
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
||||||
|
col.className = cls;
|
||||||
|
}
|
||||||
|
// adds sort indicators for current column being sorted
|
||||||
|
function addSortIndicators() {
|
||||||
|
getNthColumn(currentSort.index).className += currentSort.desc
|
||||||
|
? ' sorted-desc'
|
||||||
|
: ' sorted';
|
||||||
|
}
|
||||||
|
// adds event listeners for all sorter widgets
|
||||||
|
function enableUI() {
|
||||||
|
var i,
|
||||||
|
el,
|
||||||
|
ithSorter = function ithSorter(i) {
|
||||||
|
var col = cols[i];
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
var desc = col.defaultDescSort;
|
||||||
|
|
||||||
|
if (currentSort.index === i) {
|
||||||
|
desc = !currentSort.desc;
|
||||||
|
}
|
||||||
|
sortByIndex(i, desc);
|
||||||
|
removeSortIndicators();
|
||||||
|
currentSort.index = i;
|
||||||
|
currentSort.desc = desc;
|
||||||
|
addSortIndicators();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
for (i = 0; i < cols.length; i += 1) {
|
||||||
|
if (cols[i].sortable) {
|
||||||
|
// add the click event handler on the th so users
|
||||||
|
// dont have to click on those tiny arrows
|
||||||
|
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
||||||
|
if (el.addEventListener) {
|
||||||
|
el.addEventListener('click', ithSorter(i));
|
||||||
|
} else {
|
||||||
|
el.attachEvent('onclick', ithSorter(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// adds sorting functionality to the UI
|
||||||
|
return function() {
|
||||||
|
if (!getTable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cols = loadColumns();
|
||||||
|
loadData();
|
||||||
|
addSearchBox();
|
||||||
|
addSortIndicators();
|
||||||
|
enableUI();
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
window.addEventListener('load', addSorting);
|
||||||
@ -1,78 +0,0 @@
|
|||||||
{
|
|
||||||
"document": {
|
|
||||||
"id": "doc-1757978779922",
|
|
||||||
"fileName": "IMG_20250902_162210.jpg",
|
|
||||||
"fileSize": 980755,
|
|
||||||
"mimeType": "image/jpeg",
|
|
||||||
"uploadTimestamp": "2025-09-15T23:26:19.922Z"
|
|
||||||
},
|
|
||||||
"classification": {
|
|
||||||
"documentType": "Document",
|
|
||||||
"confidence": 0.6,
|
|
||||||
"subType": "Document",
|
|
||||||
"language": "fr",
|
|
||||||
"pageCount": 1
|
|
||||||
},
|
|
||||||
"extraction": {
|
|
||||||
"text": {
|
|
||||||
"raw": "4 . al -e ge :\na 4 mT. a ES Se Gaga EES hoe i -\na\nLo ee\nBaie: JERE REEMISEEE -\n1 ss : :-\n1) Carevalablejusques: 17.01.2033 À\n3 déévét: 1801 2018 : Ë 3\n\n Ban a / J\n\nPoa mec penses . -\n",
|
|
||||||
"processed": "4 . al -e ge :\na 4 mT. a ES Se Gaga EES hoe i -\na\nLo ee\nBaie: JERE REEMISEEE -\nl ss : :-\nl) Carevalablejusques: l7.ol.2oee À\ne déévét: l8ol 2ol8 : Ë e\n\n Ban a / J\n\nPoa mec penses . -\n",
|
|
||||||
"wordCount": 48,
|
|
||||||
"characterCount": 338,
|
|
||||||
"confidence": 0.28
|
|
||||||
},
|
|
||||||
"entities": {
|
|
||||||
"persons": [
|
|
||||||
{
|
|
||||||
"id": "identity-0",
|
|
||||||
"type": "person",
|
|
||||||
"firstName": "Se",
|
|
||||||
"lastName": "Gaga",
|
|
||||||
"role": null,
|
|
||||||
"email": null,
|
|
||||||
"phone": null,
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"companies": [],
|
|
||||||
"addresses": [],
|
|
||||||
"financial": {
|
|
||||||
"amounts": [],
|
|
||||||
"totals": {},
|
|
||||||
"payment": {}
|
|
||||||
},
|
|
||||||
"dates": [],
|
|
||||||
"contractual": {
|
|
||||||
"clauses": [],
|
|
||||||
"signatures": []
|
|
||||||
},
|
|
||||||
"references": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"processing": {
|
|
||||||
"engine": "4NK_IA_Backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"processingTime": "4948ms",
|
|
||||||
"ocrEngine": "tesseract.js",
|
|
||||||
"nerEngine": "rule-based",
|
|
||||||
"preprocessing": {
|
|
||||||
"applied": true,
|
|
||||||
"reason": "Image preprocessing applied"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"quality": {
|
|
||||||
"globalConfidence": 0.6,
|
|
||||||
"textExtractionConfidence": 0.28,
|
|
||||||
"entityExtractionConfidence": 0.9,
|
|
||||||
"classificationConfidence": 0.6
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"success": true,
|
|
||||||
"errors": [],
|
|
||||||
"warnings": ["Aucune signature détectée"],
|
|
||||||
"timestamp": "2025-09-15T23:26:19.922Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
echo "🔍 Diagnostic du démarrage du site 4NK_IA_front"
|
|
||||||
echo "================================================"
|
|
||||||
|
|
||||||
echo "📁 Répertoire actuel:"
|
|
||||||
pwd
|
|
||||||
|
|
||||||
echo -e "\n📦 Vérification des fichiers essentiels:"
|
|
||||||
ls -la package.json 2>/dev/null && echo "✅ package.json trouvé" || echo "❌ package.json manquant"
|
|
||||||
ls -la vite.config.ts 2>/dev/null && echo "✅ vite.config.ts trouvé" || echo "❌ vite.config.ts manquant"
|
|
||||||
ls -la src/ 2>/dev/null && echo "✅ dossier src/ trouvé" || echo "❌ dossier src/ manquant"
|
|
||||||
|
|
||||||
echo -e "\n🔧 Vérification de Node.js:"
|
|
||||||
node --version 2>/dev/null && echo "✅ Node.js disponible" || echo "❌ Node.js non trouvé"
|
|
||||||
npm --version 2>/dev/null && echo "✅ npm disponible" || echo "❌ npm non trouvé"
|
|
||||||
|
|
||||||
echo -e "\n📋 Vérification des dépendances:"
|
|
||||||
if [ -d "node_modules" ]; then
|
|
||||||
echo "✅ node_modules/ existe"
|
|
||||||
ls node_modules/ | wc -l | xargs echo "📊 Nombre de packages installés:"
|
|
||||||
else
|
|
||||||
echo "❌ node_modules/ manquant - exécutez: npm install"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "\n🌐 Vérification des ports:"
|
|
||||||
ss -tlnp | grep 5174 && echo "⚠️ Port 5174 déjà utilisé" || echo "✅ Port 5174 libre"
|
|
||||||
|
|
||||||
echo -e "\n🚀 Tentative de démarrage:"
|
|
||||||
echo "Exécution de: npm run dev"
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
|
||||||
frontend:
|
|
||||||
image: 'git.4nkweb.com/4nk/4nk-ia-front:${TAG:-dev}'
|
|
||||||
container_name: 4nk-ia-front
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- '8080:80'
|
|
||||||
environment:
|
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://172.23.0.10:8000}
|
|
||||||
healthcheck:
|
|
||||||
test: ['CMD', 'wget', '-qO-', 'http://localhost/']
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 10s
|
|
||||||
@ -1,244 +0,0 @@
|
|||||||
# Journal d'incident - 2025-09-16
|
|
||||||
|
|
||||||
## Mise à jour – 2025-09-18
|
|
||||||
|
|
||||||
### État d’avancement
|
|
||||||
|
|
||||||
- Backend: support explicite des `.txt`, upgrade `multer@^2`, sécurisation de tous les accès `.length` (OCR/NER), traitement asynchrone robuste (flags `.pending`, nettoyage garanti).
|
|
||||||
- Cache unifié: usage de `cache/` à la racine; backup puis purge de `backend/cache/` (doublon) pour éviter les incohérences.
|
|
||||||
- Outils: `scripts/precache.cjs` pour préremplir le cache à partir d’`uploads/` (détection root/backend automatique).
|
|
||||||
- Documentation: ajout `docs/CACHE_ET_TRAITEMENT_ASYNC.md` et enrichissement de l’analyse.
|
|
||||||
- Frontend: code splitting confirmé (`React.lazy`/`Suspense`), React Router v7, MUI v7, Redux Toolkit. Nouvelles commandes de tests: `test:collectors`, `test:ocr`, `test:api`, `test:e2e`, `test:all`.
|
|
||||||
|
|
||||||
### Conformité aux bonnes pratiques
|
|
||||||
|
|
||||||
- Qualité: ESLint 9, Prettier, markdownlint; Vitest + Testing Library; `tsconfig` strict; Docker multi-stage avec Nginx pour SPA.
|
|
||||||
- Architecture: couche services HTTP (Axios) isolée, état centralisé (Redux Toolkit), routing moderne, mécanismes de cache et asynchronisme documentés.
|
|
||||||
|
|
||||||
### Risques et points de vigilance
|
|
||||||
|
|
||||||
- Dépendance suspecte `router-dom` (doublon de `react-router-dom`) dans `package.json` racine: à supprimer si non utilisée.
|
|
||||||
- Alignement de types: vérifier la stricte conformité entre `ExtractionResult` (front) et la réponse normalisée backend (ex. champs additionnels comme `timestamp`).
|
|
||||||
- Rigueur markdownlint: s’assurer des lignes vides autour des titres/blocs et de longueurs de ligne raisonnables dans les nouveaux docs.
|
|
||||||
- CI/Tagging: respecter le préfixe de commit `ci: docker_tag=dev-test` et les conventions internes.
|
|
||||||
|
|
||||||
### Actions prioritaires
|
|
||||||
|
|
||||||
1. Mettre à jour `CHANGELOG.md` (support `.txt`, durcissement `.length`, cache unifié, script precache, doc async/cache).
|
|
||||||
2. Lancer `npm run lint`, `npm run mdlint`, `npm run test:all`, `npm run build` et corriger les erreurs TS/ESLint éventuelles (types d’entités, variables inutilisées, deps de hooks).
|
|
||||||
3. Retirer `router-dom` si non utilisée.
|
|
||||||
4. Committer et pousser sur `dev` avec message CI conforme; proposer un tag.
|
|
||||||
|
|
||||||
## Résumé
|
|
||||||
|
|
||||||
Le frontend affichait des erreurs 502 Bad Gateway via Nginx pour les endpoints `/api/health` et `/api/folders/{hash}/results`.
|
|
||||||
|
|
||||||
## Constat
|
|
||||||
|
|
||||||
- `curl https://ia.4nkweb.com/api/health` retournait 502.
|
|
||||||
- Aucun service n'écoutait en local sur le port 3001.
|
|
||||||
- `backend.log` montrait des tentatives de traitement sur un fichier `.txt` avec erreurs Sharp (format non supporté), indiquant un arrêt préalable du service.
|
|
||||||
|
|
||||||
## Actions de rétablissement
|
|
||||||
|
|
||||||
1. Installation de Node via nvm (Node 22.x) pour satisfaire `engines`.
|
|
||||||
2. Installation des dépendances (`npm ci`) côté backend et racine.
|
|
||||||
3. Démarrage du backend Express sur 3001 en arrière‑plan avec logs et PID.
|
|
||||||
4. Vérifications:
|
|
||||||
- `curl http://127.0.0.1:3001/api/health` → 200 OK
|
|
||||||
- `curl https://ia.4nkweb.com/api/health` → 200 OK
|
|
||||||
- `curl https://ia.4nkweb.com/api/folders/7d99a85daf66a0081a0e881630e6b39b/results` → 200 OK
|
|
||||||
|
|
||||||
## Causes probables
|
|
||||||
|
|
||||||
- Processus backend arrêté (pas de listener sur 3001).
|
|
||||||
- Gestion incomplète des fichiers `.txt` côté backend (détection MIME), pouvant entraîner des erreurs avec Sharp.
|
|
||||||
|
|
||||||
## Recommandations de suivi
|
|
||||||
|
|
||||||
- Ajouter le mapping `.txt` → `text/plain` dans la détection MIME backend.
|
|
||||||
- Dans `/api/extract`, gérer explicitement les `.txt` (lecture directe) comme dans `processDocument`.
|
|
||||||
- Mettre à jour Multer vers 2.x (1.x est vulnérable et déprécié).
|
|
||||||
- Surveiller `backend.log` et ajouter une supervision (systemd/service manager).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Audit détaillé du dépôt 4NK_IA_front
|
|
||||||
|
|
||||||
#### Portée
|
|
||||||
|
|
||||||
- **Code analysé**: frontend React/TypeScript sous Vite, répertoire `/home/debian/4NK_IA_front`.
|
|
||||||
- **Commandes exécutées**: `npm ci`, `npm run lint`, `npm run mdlint`, `npm run test`, `npm run build`.
|
|
||||||
|
|
||||||
### Architecture et pile technique
|
|
||||||
|
|
||||||
- **Framework**: React 19 + TypeScript, Vite 7 (`vite.config.ts`).
|
|
||||||
- **UI**: MUI v7.
|
|
||||||
- **État**: Redux Toolkit + `react-redux` (`src/store/index.ts`, `src/store/documentSlice.ts`).
|
|
||||||
- **Routing**: React Router v7 avec code-splitting via `React.lazy`/`Suspense` (`src/router/index.tsx`).
|
|
||||||
- **Services**: couche d’abstraction HTTP via Axios (`src/services/api.ts`), backend direct (`src/services/backendApi.ts`), OpenAI (`src/services/openai.ts`), fichiers/ dossiers (`src/services/folderApi.ts`).
|
|
||||||
- **Build/Runtime**: Docker multi‑stage Node→Nginx, config SPA (`Dockerfile`, `nginx.conf`).
|
|
||||||
- **Qualité**: ESLint + Prettier + markdownlint, Vitest + Testing Library.
|
|
||||||
|
|
||||||
### Points forts
|
|
||||||
|
|
||||||
- **Code splitting** déjà en place au niveau du routeur.
|
|
||||||
- **Centralisation d’état** propre (slices, middlewares, persistance localStorage).
|
|
||||||
- **Abstraction des services** HTTP claire (Axios + backends alternatifs).
|
|
||||||
- **Docker** production prêt (Nginx + healthcheck) et CI potentielle via `docker-compose.registry.yml`.
|
|
||||||
|
|
||||||
### Problèmes détectés
|
|
||||||
|
|
||||||
#### Lint TypeScript/JS
|
|
||||||
|
|
||||||
- Commande: `npm run lint`.
|
|
||||||
- Résultat: 57 problèmes trouvés (49 erreurs, 8 avertissements).
|
|
||||||
- Catégories principales:
|
|
||||||
- Types interdits `any` dans plusieurs services, ex. `src/services/backendApi.ts` (lignes 23–36, 90, 97, 107) et `src/services/fileExtract.ts` (lignes 2, 5, 55, 63, 110).
|
|
||||||
- Variables non utilisées, ex. `src/App.tsx` ligne 8 (`setCurrentFolderHash`), `src/store/documentSlice.ts` lignes 7, 56, 420, 428, `src/services/openai.ts` variables `_file`, `_address`, etc.
|
|
||||||
- Règles `react-hooks/exhaustive-deps` dans `src/App.tsx` (ligne 65) et `src/components/Layout.tsx` (ligne 84).
|
|
||||||
- Regex avec échappements inutiles dans `src/services/ruleNer.ts` (lignes 33–34, 84, 119).
|
|
||||||
- Typage middleware Redux avec `any` dans `src/store/index.ts` ligne 8.
|
|
||||||
|
|
||||||
#### Lint Markdown
|
|
||||||
|
|
||||||
- Commande: `npm run mdlint`.
|
|
||||||
- Problèmes récurrents:
|
|
||||||
- Manque de lignes vides autour des titres et listes: `CHANGELOG.md`, `docs/API_BACKEND.md`, `docs/architecture-backend.md`, `docs/changelog-pending.md`, `docs/HASH_SYSTEM.md`.
|
|
||||||
- Longueur de ligne > 120 caractères: `docs/API_BACKEND.md`, `docs/architecture-backend.md`, `docs/HASH_SYSTEM.md`, `docs/systeme-pending.md`.
|
|
||||||
- Blocs de code sans langage ou sans lignes vides autour.
|
|
||||||
- Titres en emphase non conformes (MD036) dans certains documents.
|
|
||||||
|
|
||||||
#### Tests
|
|
||||||
|
|
||||||
- Commande: `npm run test`.
|
|
||||||
- Résultat: 1 suite OK, 1 suite en échec.
|
|
||||||
- Échec: `tests/testFilesApi.test.ts` — import introuvable `../src/services/testFilesApi` (le fichier n’existe pas). Il faut soit créer `src/services/testFilesApi.ts`, soit adapter le test pour la source disponible.
|
|
||||||
|
|
||||||
#### Build de production
|
|
||||||
|
|
||||||
- Commande: `npm run build`.
|
|
||||||
- Résultat: erreurs TypeScript empêchant la compilation.
|
|
||||||
- Incohérences de types côté `ExtractionResult` consommé vs structures produites dans `src/services/api.ts` (propriété `timestamp` non prévue dans `ExtractionResult`).
|
|
||||||
- Types d’entités utilisés en `views/ExtractionView.tsx` traités comme `string` au lieu d’objets typés (`Identity`, `Address`). Ex: accès à `firstName`, `lastName`, `street`, `city`, `postalCode`, `confidence` sur des `string`.
|
|
||||||
- Variables non utilisées dans plusieurs fichiers (cf. lint ci‑dessus).
|
|
||||||
|
|
||||||
### Causes probables et pistes de résolution
|
|
||||||
|
|
||||||
- **Modèle de données**: divergence entre la structure standardisée `ExtractionResult` (`src/types/index.ts`) et le mapping réalisé dans `src/services/api.ts`/`backendApi.ts`. Il faut:
|
|
||||||
- Aligner les champs (ex. déplacer `timestamp` vers `status.timestamp` ou vers des métadonnées conformes).
|
|
||||||
- Harmoniser la forme des entités retournées (personnes/adresses) pour correspondre strictement à `Identity[]` et `Address[]`.
|
|
||||||
- **Composants de vues**: `ExtractionView.tsx` suppose des entités objets alors que les données mappées peuvent contenir des chaînes. Il faut normaliser en amont (mapping service) et/ou renforcer les garde‑fous de rendu.
|
|
||||||
- **Tests**: ajouter `src/services/testFilesApi.ts` (ou ajuster l’import) pour couvrir l’API de fichiers de test référencée par `tests/testFilesApi.test.ts`.
|
|
||||||
- **Qualité**:
|
|
||||||
- Remplacer les `any` par des types précis (ou `unknown` + raffinements), surtout dans `backendApi.ts`, `fileExtract.ts`, `openai.ts`.
|
|
||||||
- Corriger les dépendances de hooks React.
|
|
||||||
- Nettoyer les variables non utilisées et directives `eslint-disable` superflues.
|
|
||||||
- Corriger les regex avec échappements inutiles dans `ruleNer.ts`.
|
|
||||||
- Corriger les erreurs markdown (MD013, MD022, MD031, MD032, MD036, MD040, MD047) en ajoutant lignes vides et langages de blocs.
|
|
||||||
|
|
||||||
### Sécurité et configuration
|
|
||||||
|
|
||||||
- **Variables d’environnement**: `VITE_API_URL`, `VITE_USE_OPENAI`, clés OpenAI masquées en dev (`src/services/api.ts`). OK.
|
|
||||||
- **Nginx**: SPA et healthcheck définis (`nginx.conf`). OK pour production statique.
|
|
||||||
- **Docker**: image multi‑stage; healthcheck HTTP. Tagging via scripts et `docker-compose.registry.yml` (variable `TAG`). À aligner avec conventions internes du registre.
|
|
||||||
|
|
||||||
### Recommandations prioritaires (ordre d’exécution)
|
|
||||||
|
|
||||||
1. Corriger les erreurs TypeScript bloquantes du build:
|
|
||||||
- Aligner `ExtractionResult` consommé/produit (services + vues). Supprimer/relocaliser `timestamp` au bon endroit.
|
|
||||||
- Normaliser `identities`/`addresses` en objets typés dans le mapping service.
|
|
||||||
- Corriger `ExtractionView.tsx` pour refléter les types réels.
|
|
||||||
2. Supprimer variables non utilisées et corriger `any` majeurs dans services critiques (`backendApi.ts`, `fileExtract.ts`).
|
|
||||||
3. Ajouter/implémenter `src/services/testFilesApi.ts` ou corriger l’import de test.
|
|
||||||
4. Corriger les règles `react-hooks/exhaustive-deps`.
|
|
||||||
5. Corriger markdownlint dans `CHANGELOG.md` et `docs/*.md` (lignes vides, langages de blocs, longueurs de ligne raisonnables).
|
|
||||||
6. Relancer lint, tests et build pour valider.
|
|
||||||
|
|
||||||
### Notes de compatibilité
|
|
||||||
|
|
||||||
- **Node**: engines `>=20.19 <23`. Testé avec Node 22.12.0 (OK) conformément au README.
|
|
||||||
- **ESLint**: config moderne (eslint@9, typescript-eslint@8) — stricte sur `any` et hooks React.
|
|
||||||
|
|
||||||
### Annexes (références de fichiers)
|
|
||||||
|
|
||||||
- `src/types/index.ts` — définitions `ExtractionResult`, `Identity`, `Address`.
|
|
||||||
- `src/services/api.ts` — mapping de la réponse backend; contient la propriété non typée `timestamp` sur `ExtractionResult` (à déplacer).
|
|
||||||
- `src/views/ExtractionView.tsx` — accès de propriétés d’objets sur des `string` (à corriger après normalisation du mapping).
|
|
||||||
- `tests/testFilesApi.test.ts` — dépend de `src/services/testFilesApi.ts` non présent.
|
|
||||||
|
|
||||||
## Mise à jour – 2025-09-19
|
|
||||||
|
|
||||||
### État du dépôt et de la build
|
|
||||||
|
|
||||||
- Build Vite/TypeScript: OK (production) — artefacts générés dans `dist/`.
|
|
||||||
- Lint JS/TS: 134 erreurs, 9 avertissements (principalement `no-explicit-any`, deps de hooks, `no-empty`).
|
|
||||||
- Lint Markdown: nombreuses erreurs dans `docs/` et dans des dépendances tierces sous `backend/node_modules/`.
|
|
||||||
|
|
||||||
### Détails des constats
|
|
||||||
|
|
||||||
- Router: découpage de code via `React.lazy` et `Suspense` dans `src/router/index.tsx`.
|
|
||||||
- État: centralisé via Redux Toolkit, persistance `localStorage` (`src/store/index.ts`).
|
|
||||||
- Services: séparation claire (`src/services/api.ts`, `backendApi.ts`, `folderApi.ts`, `openai.ts`).
|
|
||||||
- Vues: `UploadView`, `ExtractionView`, `AnalyseView`, `ContexteView`, `ConseilView`.
|
|
||||||
|
|
||||||
### Résultats outillés
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Lint JS/TS
|
|
||||||
npm run lint
|
|
||||||
|
|
||||||
# Lint Markdown
|
|
||||||
npm run mdlint
|
|
||||||
|
|
||||||
# Build prod
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Tests unitaires
|
|
||||||
npm run test
|
|
||||||
```
|
|
||||||
|
|
||||||
Observations clés:
|
|
||||||
|
|
||||||
- `npm run build`: succès, aucun blocage de typage.
|
|
||||||
- `npm run lint`: erreurs récurrentes `@typescript-eslint/no-explicit-any` dans:
|
|
||||||
- `src/services/backendApi.ts`, `src/services/fileExtract.ts`, `src/views/*`, `src/store/*`.
|
|
||||||
- Règles hooks: `react-hooks/exhaustive-deps` (`App.tsx`, `Layout.tsx`, `UploadView.tsx`).
|
|
||||||
- `npm run mdlint`: le linter inspecte des fichiers tiers dans `backend/node_modules/`. À ignorer côté script.
|
|
||||||
|
|
||||||
### Tests (Vitest)
|
|
||||||
|
|
||||||
- Suites en échec: `tests/ocr.test.js`, `tests/collectors.test.js`, `tests/ExtractionView.tabs.test.tsx`.
|
|
||||||
- Erreurs d’import pour `supertest` dans `tests/api.test.js` et `tests/e2e.test.js`.
|
|
||||||
- Problèmes identifiés:
|
|
||||||
- `ExtractionView.tabs.test.tsx`: `useNavigate` hors Router → utiliser `MemoryRouter`.
|
|
||||||
- `api.test.js` / `e2e.test.js`: dépendance manquante `supertest`.
|
|
||||||
- `ocr.test.js`: fichiers images manquants (Sharp) → fournir fixtures ou mocker I/O.
|
|
||||||
- `collectors.test.js`: timeouts et attentes non alignées → mock `fetch` et ajuster assertions.
|
|
||||||
- Port 3001 occupé pendant les tests (EADDRINUSE) → ne pas démarrer de serveur réel, utiliser des doubles/ports éphémères.
|
|
||||||
|
|
||||||
### Recommandations immédiates (ordre conseillé)
|
|
||||||
|
|
||||||
1. Qualité TypeScript
|
|
||||||
- Remplacer `any` par des types précis (ou `unknown` avec affinage) dans `backendApi.ts`, `fileExtract.ts`, `views/*`, `store/*`.
|
|
||||||
- Corriger les dépendances de hooks (`react-hooks/exhaustive-deps`).
|
|
||||||
2. Tests
|
|
||||||
- Ajouter `devDependency` `supertest` ou conditionner les tests d’API.
|
|
||||||
- Enrober les rendus dépendants du router par `MemoryRouter`.
|
|
||||||
- Mocker les appels réseaux (collectors) et I/O (OCR/Sharp).
|
|
||||||
- Éviter l’écoute du port 3001 pendant les tests.
|
|
||||||
3. Markdownlint
|
|
||||||
- Mettre à jour le script `mdlint` pour ignorer `backend/node_modules` et autres dossiers de dépendances (appr. requise).
|
|
||||||
4. Documentation
|
|
||||||
- Poursuivre l’alignement sur `ExtractionResult` et documenter les champs backend réels dans `docs/API_BACKEND.md`.
|
|
||||||
|
|
||||||
### Demandes d’approbation
|
|
||||||
|
|
||||||
- Puis-je modifier `package.json` (script `mdlint`) pour ajouter `--ignore backend/node_modules` ?
|
|
||||||
- Souhaitez-vous ajouter `supertest` en `devDependency` afin de rétablir `tests/api.test.js` et `tests/e2e.test.js` ?
|
|
||||||
|
|
||||||
### Prochaines actions proposées
|
|
||||||
|
|
||||||
- Corriger une première tranche d’erreurs `any` dans `src/services/backendApi.ts` et `src/store/index.ts`.
|
|
||||||
- Mettre à jour `tests/ExtractionView.tabs.test.tsx` pour utiliser `MemoryRouter`.
|
|
||||||
- Ajouter des mocks pour `fetch` dans `tests/collectors.test.js` et des fixtures/mocks OCR.
|
|
||||||
310
docs/API.md
310
docs/API.md
@ -1,9 +1,9 @@
|
|||||||
# Documentation API - IA Lecoffre.io
|
# Documentation API - 4NK IA Front Notarial
|
||||||
|
|
||||||
## Vue d'ensemble
|
## Vue d'ensemble
|
||||||
|
|
||||||
L'application IA Lecoffre.io communique uniquement avec le backend interne pour toutes les
|
L'application 4NK IA Front Notarial communique avec plusieurs APIs pour fournir une expérience complète
|
||||||
fonctionnalités (upload, extraction, analyse, contexte, conseil).
|
d'analyse de documents notariaux.
|
||||||
|
|
||||||
## API Backend Principal
|
## API Backend Principal
|
||||||
|
|
||||||
@ -18,41 +18,86 @@ http://localhost:8000 (développement)
|
|||||||
#### Upload de document
|
#### Upload de document
|
||||||
|
|
||||||
```http
|
```http
|
||||||
POST /api/notary/upload
|
POST /api/documents/upload
|
||||||
Content-Type: multipart/form-data
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
Body: FormData avec le fichier
|
Body: FormData avec le fichier
|
||||||
```
|
```
|
||||||
|
|
||||||
Réponse attendue (champs utilisés par le front) :
|
**Réponse :**
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"document_id": "doc_123456",
|
|
||||||
"mime_type": "application/pdf",
|
|
||||||
"functional_type": "CNI"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Mappage front en `Document` :
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "doc_123456",
|
"id": "doc_123456",
|
||||||
"name": "acte_vente.pdf",
|
"name": "acte_vente.pdf",
|
||||||
"mimeType": "application/pdf",
|
"type": "application/pdf",
|
||||||
"functionalType": "CNI",
|
|
||||||
"size": 1024000,
|
"size": 1024000,
|
||||||
"uploadDate": "<date locale>",
|
"uploadDate": "2024-01-15T10:30:00Z",
|
||||||
"status": "completed",
|
"status": "completed"
|
||||||
"previewUrl": "blob:..."
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Extraction de données
|
#### Extraction de données
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /api/notary/documents/{documentId}
|
GET /api/documents/{documentId}/extract
|
||||||
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"documentId": "doc_123456",
|
||||||
|
"text": "Texte extrait du document...",
|
||||||
|
"language": "fr",
|
||||||
|
"documentType": "Acte de vente",
|
||||||
|
"identities": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"type": "person",
|
||||||
|
"firstName": "Jean",
|
||||||
|
"lastName": "Dupont",
|
||||||
|
"birthDate": "1980-05-15",
|
||||||
|
"nationality": "Française",
|
||||||
|
"confidence": 0.95
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"addresses": [
|
||||||
|
{
|
||||||
|
"street": "123 Rue de la Paix",
|
||||||
|
"city": "Paris",
|
||||||
|
"postalCode": "75001",
|
||||||
|
"country": "France"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"type": "apartment",
|
||||||
|
"address": {
|
||||||
|
"street": "123 Rue de la Paix",
|
||||||
|
"city": "Paris",
|
||||||
|
"postalCode": "75001",
|
||||||
|
"country": "France"
|
||||||
|
},
|
||||||
|
"surface": 75,
|
||||||
|
"cadastralReference": "1234567890AB",
|
||||||
|
"value": 250000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contracts": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"type": "sale",
|
||||||
|
"parties": [],
|
||||||
|
"amount": 250000,
|
||||||
|
"date": "2024-01-15",
|
||||||
|
"clauses": ["Clause de garantie", "Clause de condition suspensive"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"signatures": ["Jean Dupont", "Marie Martin"],
|
||||||
|
"confidence": 0.92
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Analyse du document
|
#### Analyse du document
|
||||||
@ -61,38 +106,128 @@ GET /api/notary/documents/{documentId}
|
|||||||
GET /api/documents/{documentId}/analyze
|
GET /api/documents/{documentId}/analyze
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"documentId": "doc_123456",
|
||||||
|
"documentType": "Acte de vente",
|
||||||
|
"isCNI": false,
|
||||||
|
"credibilityScore": 0.88,
|
||||||
|
"summary": "Document analysé avec succès. Toutes les informations semblent cohérentes.",
|
||||||
|
"recommendations": [
|
||||||
|
"Vérifier l'identité des parties auprès des autorités compétentes",
|
||||||
|
"Contrôler la validité des documents cadastraux"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### Données contextuelles
|
#### Données contextuelles
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /api/documents/{documentId}/context
|
GET /api/documents/{documentId}/context
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"documentId": "doc_123456",
|
||||||
|
"cadastreData": {
|
||||||
|
"status": "disponible",
|
||||||
|
"reference": "1234567890AB"
|
||||||
|
},
|
||||||
|
"georisquesData": {
|
||||||
|
"status": "aucun risque identifié"
|
||||||
|
},
|
||||||
|
"geofoncierData": {
|
||||||
|
"status": "données disponibles"
|
||||||
|
},
|
||||||
|
"bodaccData": {
|
||||||
|
"status": "aucune procédure en cours"
|
||||||
|
},
|
||||||
|
"infogreffeData": {
|
||||||
|
"status": "entreprise en règle"
|
||||||
|
},
|
||||||
|
"lastUpdated": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### Conseil IA
|
#### Conseil IA
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /api/documents/{documentId}/conseil
|
GET /api/documents/{documentId}/conseil
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Réponse :**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"documentId": "doc_123456",
|
||||||
|
"analysis": "Ce document présente toutes les caractéristiques d'un acte notarial standard.",
|
||||||
|
"recommendations": [
|
||||||
|
"Procéder à la vérification d'identité des parties",
|
||||||
|
"Contrôler la validité des documents fournis"
|
||||||
|
],
|
||||||
|
"risks": [
|
||||||
|
"Risque faible : Vérification d'identité recommandée",
|
||||||
|
"Risque moyen : Contrôle cadastral nécessaire"
|
||||||
|
],
|
||||||
|
"nextSteps": [
|
||||||
|
"Collecter les pièces d'identité des parties",
|
||||||
|
"Vérifier les documents cadastraux",
|
||||||
|
"Préparer l'acte final"
|
||||||
|
],
|
||||||
|
"generatedAt": "2024-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## APIs Externes
|
## APIs Externes
|
||||||
|
|
||||||
Les APIs externes (Cadastre, Géorisques, Géofoncier, BODACC, Infogreffe) sont appelées côté backend
|
### Cadastre
|
||||||
uniquement. Aucun appel direct côté front.
|
|
||||||
|
**URL :** `https://api.cadastre.gouv.fr`
|
||||||
|
**Usage :** Vérification des références cadastrales et des propriétés
|
||||||
|
|
||||||
|
### Géorisques
|
||||||
|
|
||||||
|
**URL :** `https://www.georisques.gouv.fr/api`
|
||||||
|
**Usage :** Analyse des risques géologiques et environnementaux
|
||||||
|
|
||||||
|
### Géofoncier
|
||||||
|
|
||||||
|
**URL :** `https://api.geofoncier.fr`
|
||||||
|
**Usage :** Données foncières et géographiques
|
||||||
|
|
||||||
|
### BODACC
|
||||||
|
|
||||||
|
**URL :** `https://api.bodacc.fr`
|
||||||
|
**Usage :** Vérification des procédures collectives et des entreprises
|
||||||
|
|
||||||
|
### Infogreffe
|
||||||
|
|
||||||
|
**URL :** `https://api.infogreffe.fr`
|
||||||
|
**Usage :** Informations sur les entreprises et leurs dirigeants
|
||||||
|
|
||||||
## Gestion d'erreur
|
## Gestion d'erreur
|
||||||
|
|
||||||
### Codes d'erreur HTTP
|
### Codes d'erreur HTTP
|
||||||
|
|
||||||
- 200 : Succès
|
- **200** : Succès
|
||||||
- 400 : Requête malformée
|
- **400** : Requête malformée
|
||||||
- 404 : Ressource non trouvée
|
- **404** : Ressource non trouvée
|
||||||
- 405 : Méthode non autorisée
|
- **405** : Méthode non autorisée
|
||||||
- 500 : Erreur serveur interne
|
- **500** : Erreur serveur interne
|
||||||
|
|
||||||
### Erreurs de connexion
|
### Erreurs de connexion
|
||||||
|
|
||||||
- ERR_NETWORK : Erreur de réseau
|
- **ERR_NETWORK** : Erreur de réseau
|
||||||
- ERR_CONNECTION_REFUSED : Connexion refusée
|
- **ERR_CONNECTION_REFUSED** : Connexion refusée
|
||||||
- ERR_TIMEOUT : Timeout de la requête
|
- **ERR_TIMEOUT** : Timeout de la requête
|
||||||
|
|
||||||
|
### Fallback automatique
|
||||||
|
|
||||||
|
En cas d'erreur, l'application bascule automatiquement vers des données de démonstration pour maintenir l'expérience utilisateur.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@ -100,28 +235,22 @@ uniquement. Aucun appel direct côté front.
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_API_URL=http://localhost:8000
|
VITE_API_URL=http://localhost:8000
|
||||||
VITE_USE_OPENAI=false
|
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
|
||||||
VITE_OPENAI_API_KEY=
|
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||||
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
|
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
|
||||||
VITE_OPENAI_MODEL=gpt-4o-mini
|
VITE_BODACC_API_URL=https://api.bodacc.fr
|
||||||
|
VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode OpenAI (fallback)
|
|
||||||
|
|
||||||
Quand `VITE_USE_OPENAI=true`, le frontend bascule sur un mode de secours basé sur OpenAI:
|
|
||||||
|
|
||||||
- Upload: simulé côté client (le fichier n’est pas envoyé à OpenAI)
|
|
||||||
- Extraction/Analyse/Conseil/Contexte: appels `chat.completions` sur `VITE_OPENAI_MODEL`
|
|
||||||
- Détection de type: heuristique simple côté client
|
|
||||||
|
|
||||||
Ce mode est utile pour démo/diagnostic quand le backend n’est pas disponible.
|
|
||||||
|
|
||||||
### Configuration Axios
|
### Configuration Axios
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: BASE_URL,
|
||||||
timeout: 60000,
|
timeout: 60000, // 60 secondes
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -134,6 +263,93 @@ Authorization: Bearer {token}
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Gestion des tokens
|
||||||
|
|
||||||
|
- **Refresh automatique** : Renouvellement des tokens expirés
|
||||||
|
- **Intercepteurs** : Ajout automatique des headers d'authentification
|
||||||
|
- **Logout automatique** : Déconnexion en cas d'erreur 401
|
||||||
|
|
||||||
## Rate Limiting
|
## Rate Limiting
|
||||||
|
|
||||||
- Limites gérées par le backend
|
### Limites par défaut
|
||||||
|
|
||||||
|
- **100 requêtes/minute** par utilisateur
|
||||||
|
- **1000 requêtes/heure** par utilisateur
|
||||||
|
- **Backoff exponentiel** en cas de dépassement
|
||||||
|
|
||||||
|
### Headers de réponse
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-RateLimit-Limit: 100
|
||||||
|
X-RateLimit-Remaining: 95
|
||||||
|
X-RateLimit-Reset: 1642248000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring et logs
|
||||||
|
|
||||||
|
### Métriques collectées
|
||||||
|
|
||||||
|
- **Temps de réponse** : Latence des requêtes
|
||||||
|
- **Taux d'erreur** : Pourcentage d'échecs
|
||||||
|
- **Utilisation** : Nombre de requêtes par endpoint
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
- **Requêtes** : Log de toutes les requêtes API
|
||||||
|
- **Erreurs** : Log détaillé des erreurs
|
||||||
|
- **Performance** : Métriques de performance
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Tests d'intégration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Test d'upload de document
|
||||||
|
test('should upload document successfully', async () => {
|
||||||
|
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||||
|
const result = await documentApi.upload(file)
|
||||||
|
expect(result.id).toBeDefined()
|
||||||
|
expect(result.status).toBe('completed')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests de fallback
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Test du mode démonstration
|
||||||
|
test('should return demo data on API error', async () => {
|
||||||
|
// Mock de l'erreur API
|
||||||
|
mockAxios.onPost('/api/documents/upload').reply(500)
|
||||||
|
|
||||||
|
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||||
|
const result = await documentApi.upload(file)
|
||||||
|
|
||||||
|
expect(result.id).toMatch(/^demo-/)
|
||||||
|
expect(result.name).toBe('test.pdf')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Validation des données
|
||||||
|
|
||||||
|
- **Sanitization** : Nettoyage des entrées utilisateur
|
||||||
|
- **Validation** : Vérification des types et formats
|
||||||
|
- **Escape** : Protection contre les injections
|
||||||
|
|
||||||
|
### CORS
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Configuration CORS
|
||||||
|
const corsOptions = {
|
||||||
|
origin: ['http://localhost:3000', 'https://app.4nkweb.com'],
|
||||||
|
credentials: true,
|
||||||
|
optionsSuccessStatus: 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTPS
|
||||||
|
|
||||||
|
- **Redirection automatique** : HTTP vers HTTPS en production
|
||||||
|
- **HSTS** : HTTP Strict Transport Security
|
||||||
|
- **Certificats SSL** : Certificats valides et à jour
|
||||||
|
|||||||
@ -1,463 +0,0 @@
|
|||||||
# 📡 API Backend 4NK_IA - Documentation
|
|
||||||
|
|
||||||
## 🚀 **Vue d'ensemble**
|
|
||||||
|
|
||||||
L'API Backend 4NK_IA est un service d'extraction et d'analyse de documents utilisant l'OCR (Reconnaissance Optique de Caractères) et le NER (Reconnaissance d'Entités Nommées) pour traiter automatiquement les documents PDF et images.
|
|
||||||
|
|
||||||
### **Caractéristiques principales :**
|
|
||||||
|
|
||||||
- ✅ **Support multi-format** : PDF, JPEG, PNG, TIFF, TXT
|
|
||||||
- ✅ **OCR avancé** : Tesseract.js avec préprocessing d'images
|
|
||||||
- ✅ **Extraction PDF directe** : pdf-parse pour une précision maximale
|
|
||||||
- ✅ **NER intelligent** : Reconnaissance d'entités par règles
|
|
||||||
- ✅ **Format JSON standardisé** : Structure cohérente pour tous les documents
|
|
||||||
- ✅ **Préprocessing d'images** : Amélioration automatique de la qualité OCR
|
|
||||||
- ✅ **Gestion des doublons** : Système de hash SHA-256 pour éviter les fichiers dupliqués
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 **Endpoints de l'API**
|
|
||||||
|
|
||||||
### **Base URL :** `http://localhost:3001/api`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 **1. Health Check**
|
|
||||||
|
|
||||||
### **GET** `/api/health`
|
|
||||||
|
|
||||||
Vérifie l'état du serveur backend.
|
|
||||||
|
|
||||||
**Réponse :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "OK",
|
|
||||||
"timestamp": "2025-09-15T22:45:50.123Z",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Exemple d'utilisation :**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:3001/api/health
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 **2. Liste des fichiers de test**
|
|
||||||
|
|
||||||
### **GET** `/api/test-files`
|
|
||||||
|
|
||||||
Retourne la liste des fichiers de test disponibles.
|
|
||||||
|
|
||||||
**Réponse :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "IMG_20250902_162159.jpg",
|
|
||||||
"size": 245760,
|
|
||||||
"type": "image/jpeg"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 **3. Liste des fichiers uploadés**
|
|
||||||
|
|
||||||
### **GET** `/api/uploads`
|
|
||||||
|
|
||||||
Retourne la liste des fichiers uploadés avec leurs métadonnées et hash SHA-256.
|
|
||||||
|
|
||||||
**Réponse :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "document-1757980637671.pdf",
|
|
||||||
"size": 1015808,
|
|
||||||
"hash": "a1b2c3d4e5f6...",
|
|
||||||
"uploadDate": "2025-09-15T23:56:14.751Z",
|
|
||||||
"modifiedDate": "2025-09-15T23:56:14.751Z"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"count": 1,
|
|
||||||
"totalSize": 1015808
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Exemple d'utilisation :**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:3001/api/uploads
|
|
||||||
```
|
|
||||||
|
|
||||||
**Notes :**
|
|
||||||
|
|
||||||
- Le hash SHA-256 permet d'identifier les fichiers identiques
|
|
||||||
- Les fichiers dupliqués sont automatiquement détectés lors de l'upload
|
|
||||||
- Seuls les fichiers uniques sont conservés dans le système
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 **4. Extraction de documents**
|
|
||||||
|
|
||||||
### **POST** `/api/extract`
|
|
||||||
|
|
||||||
Extrait et analyse un document (PDF, image ou texte) pour identifier les entités et informations structurées.
|
|
||||||
|
|
||||||
#### **Paramètres :**
|
|
||||||
|
|
||||||
- **`document`** (file, required) : Fichier à analyser (PDF, JPEG, PNG, TIFF, TXT)
|
|
||||||
- **Taille maximale :** 10MB
|
|
||||||
|
|
||||||
#### **Gestion des doublons :**
|
|
||||||
|
|
||||||
- Le système calcule automatiquement un hash SHA-256 de chaque fichier uploadé
|
|
||||||
- Si un fichier avec le même hash existe déjà, le doublon est supprimé
|
|
||||||
- Le traitement utilise le fichier existant, évitant ainsi les calculs redondants
|
|
||||||
- Les logs indiquent clairement si un fichier est nouveau ou déjà existant
|
|
||||||
|
|
||||||
#### **Réponse - Format JSON Standard :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"document": {
|
|
||||||
"id": "doc-1757976350123",
|
|
||||||
"fileName": "facture_4NK_08-2025_04.pdf",
|
|
||||||
"fileSize": 85819,
|
|
||||||
"mimeType": "application/pdf",
|
|
||||||
"uploadTimestamp": "2025-09-15T22:45:50.123Z"
|
|
||||||
},
|
|
||||||
"classification": {
|
|
||||||
"documentType": "Facture",
|
|
||||||
"confidence": 0.95,
|
|
||||||
"subType": "Facture de prestation",
|
|
||||||
"language": "fr",
|
|
||||||
"pageCount": 1
|
|
||||||
},
|
|
||||||
"extraction": {
|
|
||||||
"text": {
|
|
||||||
"raw": "Janin Consulting - EURL au capital de 500 Euros...",
|
|
||||||
"processed": "Janin Consulting - EURL au capital de soo Euros...",
|
|
||||||
"wordCount": 165,
|
|
||||||
"characterCount": 1197,
|
|
||||||
"confidence": 0.95
|
|
||||||
},
|
|
||||||
"entities": {
|
|
||||||
"persons": [
|
|
||||||
{
|
|
||||||
"id": "identity-0",
|
|
||||||
"type": "person",
|
|
||||||
"firstName": "Anthony",
|
|
||||||
"lastName": "Janin",
|
|
||||||
"role": "Gérant",
|
|
||||||
"email": "ja.janin.anthony@gmail.com",
|
|
||||||
"phone": "33 (0)6 71 40 84 13",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"companies": [
|
|
||||||
{
|
|
||||||
"id": "company-0",
|
|
||||||
"name": "Janin Consulting",
|
|
||||||
"legalForm": "EURL",
|
|
||||||
"siret": "815 322 912 00040",
|
|
||||||
"rcs": "815 322 912 NANTERRE",
|
|
||||||
"tva": "FR64 815 322 912",
|
|
||||||
"capital": "500 Euros",
|
|
||||||
"role": "Fournisseur",
|
|
||||||
"confidence": 0.95,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"addresses": [
|
|
||||||
{
|
|
||||||
"id": "address-0",
|
|
||||||
"type": "siège_social",
|
|
||||||
"street": "177 rue du Faubourg Poissonnière",
|
|
||||||
"city": "Paris",
|
|
||||||
"postalCode": "75009",
|
|
||||||
"country": "France",
|
|
||||||
"company": "Janin Consulting",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"financial": {
|
|
||||||
"amounts": [
|
|
||||||
{
|
|
||||||
"id": "amount-0",
|
|
||||||
"type": "prestation",
|
|
||||||
"description": "Prestation du mois d'Août 2025",
|
|
||||||
"quantity": 10,
|
|
||||||
"unitPrice": 550.0,
|
|
||||||
"totalHT": 5500.0,
|
|
||||||
"currency": "EUR",
|
|
||||||
"confidence": 0.95
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"totals": {
|
|
||||||
"totalHT": 5500.0,
|
|
||||||
"totalTVA": 1100.0,
|
|
||||||
"totalTTC": 6600.0,
|
|
||||||
"tvaRate": 0.2,
|
|
||||||
"currency": "EUR"
|
|
||||||
},
|
|
||||||
"payment": {
|
|
||||||
"terms": "30 jours après émission",
|
|
||||||
"penaltyRate": "Taux BCE + 7 points",
|
|
||||||
"bankDetails": {
|
|
||||||
"bank": "CAISSE D'EPARGNE D'ILE DE FRANCE",
|
|
||||||
"accountHolder": "Janin Anthony",
|
|
||||||
"address": "1 rue Pasteur (78800)",
|
|
||||||
"rib": "17515006000800309088884"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dates": [
|
|
||||||
{
|
|
||||||
"id": "date-0",
|
|
||||||
"type": "facture",
|
|
||||||
"value": "29-août-25",
|
|
||||||
"formatted": "2025-08-29",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"contractual": {
|
|
||||||
"clauses": [
|
|
||||||
{
|
|
||||||
"id": "clause-0",
|
|
||||||
"type": "paiement",
|
|
||||||
"content": "Le paiement se fera (maximum) 30 jours après l'émission de la facture.",
|
|
||||||
"confidence": 0.9
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"id": "signature-0",
|
|
||||||
"type": "électronique",
|
|
||||||
"present": false,
|
|
||||||
"signatory": null,
|
|
||||||
"date": null,
|
|
||||||
"confidence": 0.8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"id": "ref-0",
|
|
||||||
"type": "facture",
|
|
||||||
"number": "4NK_4",
|
|
||||||
"confidence": 0.95
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"processing": {
|
|
||||||
"engine": "4NK_IA_Backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"processingTime": "423ms",
|
|
||||||
"ocrEngine": "pdf-parse",
|
|
||||||
"nerEngine": "rule-based",
|
|
||||||
"preprocessing": {
|
|
||||||
"applied": false,
|
|
||||||
"reason": "PDF direct text extraction"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"quality": {
|
|
||||||
"globalConfidence": 0.95,
|
|
||||||
"textExtractionConfidence": 0.95,
|
|
||||||
"entityExtractionConfidence": 0.9,
|
|
||||||
"classificationConfidence": 0.95
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"success": true,
|
|
||||||
"errors": [],
|
|
||||||
"warnings": ["Aucune signature détectée"],
|
|
||||||
"timestamp": "2025-09-15T22:45:50.123Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Exemples d'utilisation :**
|
|
||||||
|
|
||||||
**Avec curl (PDF) :**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST \
|
|
||||||
-F "document=@/path/to/document.pdf" \
|
|
||||||
http://localhost:3001/api/extract
|
|
||||||
```
|
|
||||||
|
|
||||||
**Avec curl (TXT) :**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST \
|
|
||||||
-F "document=@/path/to/file.txt" \
|
|
||||||
http://localhost:3001/api/extract
|
|
||||||
```
|
|
||||||
|
|
||||||
**Avec JavaScript (fetch) :**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('document', fileInput.files[0])
|
|
||||||
|
|
||||||
const response = await fetch('http://localhost:3001/api/extract', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
console.log(result)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 **Types de documents supportés**
|
|
||||||
|
|
||||||
### **1. Factures**
|
|
||||||
|
|
||||||
- **Détection automatique** : Mots-clés "facture", "tva", "siren", "montant"
|
|
||||||
- **Entités extraites** :
|
|
||||||
- Sociétés (fournisseur/client)
|
|
||||||
- Adresses de facturation
|
|
||||||
- Montants et totaux
|
|
||||||
- Conditions de paiement
|
|
||||||
- Numéros de référence
|
|
||||||
- Dates
|
|
||||||
|
|
||||||
### **2. Cartes Nationales d'Identité (CNI)**
|
|
||||||
|
|
||||||
- **Détection automatique** : Mots-clés "carte nationale d'identité", "cni", "mrz"
|
|
||||||
- **Entités extraites** :
|
|
||||||
- Identités (nom, prénom)
|
|
||||||
- Numéros CNI
|
|
||||||
- Dates de naissance
|
|
||||||
- Adresses
|
|
||||||
|
|
||||||
### **3. Contrats**
|
|
||||||
|
|
||||||
- **Détection automatique** : Mots-clés "contrat", "vente", "achat", "acte"
|
|
||||||
- **Entités extraites** :
|
|
||||||
- Parties contractantes
|
|
||||||
- Clauses contractuelles
|
|
||||||
- Signatures
|
|
||||||
- Dates importantes
|
|
||||||
|
|
||||||
### **4. Attestations**
|
|
||||||
|
|
||||||
- **Détection automatique** : Mots-clés "attestation", "certificat"
|
|
||||||
- **Entités extraites** :
|
|
||||||
- Identités
|
|
||||||
- Dates
|
|
||||||
- Informations spécifiques
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 **Configuration et préprocessing**
|
|
||||||
|
|
||||||
### **Préprocessing d'images (pour JPEG, PNG, TIFF) :**
|
|
||||||
|
|
||||||
- **Redimensionnement** : Largeur cible 2000px
|
|
||||||
- **Amélioration du contraste** : Facteur 1.5
|
|
||||||
- **Luminosité** : Facteur 1.1
|
|
||||||
- **Conversion en niveaux de gris**
|
|
||||||
- **Amélioration de la netteté**
|
|
||||||
- **Réduction du bruit**
|
|
||||||
|
|
||||||
### **Extraction PDF directe :**
|
|
||||||
|
|
||||||
- **Moteur** : pdf-parse
|
|
||||||
- **Avantage** : Pas de conversion image, précision maximale
|
|
||||||
- **Confiance** : 95% par défaut
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚡ **Performances**
|
|
||||||
|
|
||||||
### **Temps de traitement typiques :**
|
|
||||||
|
|
||||||
- **PDF** : 200-500ms
|
|
||||||
- **Images** : 1-3 secondes (avec préprocessing)
|
|
||||||
- **Taille maximale** : 10MB
|
|
||||||
|
|
||||||
### **Confiance d'extraction :**
|
|
||||||
|
|
||||||
- **PDF** : 90-95%
|
|
||||||
- **Images haute qualité** : 80-90%
|
|
||||||
- **Images de qualité moyenne** : 60-80%
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚨 **Gestion d'erreurs**
|
|
||||||
|
|
||||||
### **Codes d'erreur HTTP :**
|
|
||||||
|
|
||||||
- **400** : Aucun fichier fourni
|
|
||||||
- **413** : Fichier trop volumineux (>10MB)
|
|
||||||
- **415** : Type de fichier non supporté
|
|
||||||
- **500** : Erreur de traitement interne
|
|
||||||
|
|
||||||
### **Exemple de réponse d'erreur :**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"error": "Type de fichier non supporté",
|
|
||||||
"details": "Seuls les formats PDF, JPEG, PNG et TIFF sont acceptés"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ **Dépendances techniques**
|
|
||||||
|
|
||||||
### **Moteurs OCR :**
|
|
||||||
|
|
||||||
- **Tesseract.js** : Pour les images
|
|
||||||
- **pdf-parse** : Pour les PDF
|
|
||||||
|
|
||||||
### **Préprocessing :**
|
|
||||||
|
|
||||||
- **Sharp.js** : Traitement d'images
|
|
||||||
|
|
||||||
### **NER :**
|
|
||||||
|
|
||||||
- **Règles personnalisées** : Patterns regex pour l'extraction d'entités
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 **Notes d'utilisation**
|
|
||||||
|
|
||||||
1. **Format de sortie standardisé** : Tous les documents retournent le même format JSON
|
|
||||||
2. **Confiance** : Chaque entité extraite inclut un score de confiance
|
|
||||||
3. **Source** : Indication de la méthode d'extraction (rule-based, ML, etc.)
|
|
||||||
4. **Métadonnées complètes** : Informations sur le traitement et la qualité
|
|
||||||
5. **Gestion des erreurs** : Warnings et erreurs détaillés dans la réponse
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 **Évolutions futures**
|
|
||||||
|
|
||||||
- [ ] Support de l'IA/ML pour l'extraction d'entités
|
|
||||||
- [ ] Support de documents multi-pages
|
|
||||||
- [ ] Extraction de signatures manuscrites
|
|
||||||
- [ ] Support de langues supplémentaires
|
|
||||||
- [ ] API de validation des extractions
|
|
||||||
- [ ] Cache des résultats pour optimisation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_Documentation générée le 15/09/2025 - Version 1.0.0_
|
|
||||||
282
docs/ARCHITECTURE.md
Normal file
282
docs/ARCHITECTURE.md
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
# Architecture de l'application 4NK IA Front Notarial
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
L'application 4NK IA Front Notarial est une interface web moderne construite avec React et TypeScript,
|
||||||
|
conçue pour l'analyse intelligente de documents notariaux.
|
||||||
|
|
||||||
|
## Stack technique
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
- **React 18** : Framework UI avec hooks et composants fonctionnels
|
||||||
|
- **TypeScript 5.6** : Typage statique pour la robustesse du code
|
||||||
|
- **Vite 7** : Build tool rapide avec HMR (Hot Module Replacement)
|
||||||
|
- **Material-UI v6** : Bibliothèque de composants UI professionnels
|
||||||
|
|
||||||
|
### Gestion d'état
|
||||||
|
|
||||||
|
- **Redux Toolkit** : Gestion d'état centralisée et prévisible
|
||||||
|
- **React Redux** : Liaison React-Redux avec hooks
|
||||||
|
- **Async Thunks** : Gestion des actions asynchrones
|
||||||
|
|
||||||
|
### Routing et navigation
|
||||||
|
|
||||||
|
- **React Router v6** : Navigation côté client avec code splitting
|
||||||
|
- **Lazy loading** : Chargement à la demande des composants
|
||||||
|
|
||||||
|
### HTTP et API
|
||||||
|
|
||||||
|
- **Axios** : Client HTTP avec intercepteurs
|
||||||
|
- **Intercepteurs** : Gestion centralisée des erreurs et authentification
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- **Vitest** : Framework de test rapide et moderne
|
||||||
|
- **Testing Library** : Tests d'intégration React
|
||||||
|
- **JSDOM** : Environnement DOM simulé
|
||||||
|
- **Coverage V8** : Rapport de couverture de code
|
||||||
|
|
||||||
|
### Qualité de code
|
||||||
|
|
||||||
|
- **ESLint** : Linting avec règles strictes
|
||||||
|
- **Prettier** : Formatage automatique du code
|
||||||
|
- **markdownlint** : Validation des fichiers Markdown
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
├── components/ # Composants réutilisables
|
||||||
|
│ ├── Layout.tsx # Layout principal avec AppBar et navigation
|
||||||
|
│ └── NavigationTabs.tsx # Composant de navigation par onglets
|
||||||
|
├── views/ # Vues principales de l'application
|
||||||
|
│ ├── UploadView.tsx # Upload et gestion des documents
|
||||||
|
│ ├── ExtractionView.tsx # Affichage des données extraites
|
||||||
|
│ ├── AnalyseView.tsx # Analyse et score de vraisemblance
|
||||||
|
│ ├── ContexteView.tsx # Données contextuelles externes
|
||||||
|
│ └── ConseilView.tsx # Conseil IA et recommandations
|
||||||
|
├── store/ # Gestion d'état Redux
|
||||||
|
│ ├── index.ts # Configuration du store Redux
|
||||||
|
│ ├── appSlice.ts # État global de l'application
|
||||||
|
│ └── documentSlice.ts # État des documents et opérations
|
||||||
|
├── services/ # Services et API
|
||||||
|
│ └── api.ts # Client API et endpoints
|
||||||
|
├── types/ # Types et interfaces TypeScript
|
||||||
|
│ └── index.ts # Définitions de types centralisées
|
||||||
|
├── main.tsx # Point d'entrée de l'application
|
||||||
|
└── App.tsx # Composant racine avec routing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture des composants
|
||||||
|
|
||||||
|
### Layout et navigation
|
||||||
|
|
||||||
|
- **Layout** : Composant wrapper avec AppBar et navigation
|
||||||
|
- **NavigationTabs** : Navigation par onglets avec React Router
|
||||||
|
- **AppBar** : Barre de navigation principale
|
||||||
|
|
||||||
|
### Vues principales
|
||||||
|
|
||||||
|
#### UploadView
|
||||||
|
|
||||||
|
- **Dropzone** : Zone de glisser-déposer pour les fichiers
|
||||||
|
- **FileList** : Liste des documents uploadés
|
||||||
|
- **Status indicators** : Indicateurs de statut des documents
|
||||||
|
|
||||||
|
#### ExtractionView
|
||||||
|
|
||||||
|
- **DataDisplay** : Affichage des données extraites
|
||||||
|
- **ObjectLists** : Listes d'identités, adresses, biens, contrats
|
||||||
|
- **Confidence scores** : Scores de confiance des extractions
|
||||||
|
|
||||||
|
#### AnalyseView
|
||||||
|
|
||||||
|
- **CredibilityScore** : Score de vraisemblance du document
|
||||||
|
- **Recommendations** : Liste des recommandations
|
||||||
|
- **Summary** : Synthèse de l'analyse
|
||||||
|
|
||||||
|
#### ContexteView
|
||||||
|
|
||||||
|
- **ExternalData** : Données des APIs externes
|
||||||
|
- **StatusCards** : Cartes de statut pour chaque source
|
||||||
|
- **LastUpdated** : Horodatage des dernières mises à jour
|
||||||
|
|
||||||
|
#### ConseilView
|
||||||
|
|
||||||
|
- **LLMAnalysis** : Analyse générée par l'IA
|
||||||
|
- **RiskAssessment** : Évaluation des risques
|
||||||
|
- **NextSteps** : Prochaines étapes recommandées
|
||||||
|
|
||||||
|
## Gestion d'état Redux
|
||||||
|
|
||||||
|
### Structure du store
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RootState {
|
||||||
|
app: AppState // État global de l'application
|
||||||
|
document: DocumentState // État des documents et opérations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AppState
|
||||||
|
|
||||||
|
- **status** : État global ('idle' | 'loading' | 'succeeded' | 'failed')
|
||||||
|
- **error** : Messages d'erreur globaux
|
||||||
|
|
||||||
|
### DocumentState
|
||||||
|
|
||||||
|
- **documents** : Liste des documents uploadés
|
||||||
|
- **currentDocument** : Document actuellement sélectionné
|
||||||
|
- **extractionResult** : Résultats d'extraction
|
||||||
|
- **analysisResult** : Résultats d'analyse
|
||||||
|
- **contextResult** : Données contextuelles
|
||||||
|
- **conseilResult** : Conseil IA
|
||||||
|
- **loading** : État de chargement
|
||||||
|
- **error** : Messages d'erreur
|
||||||
|
|
||||||
|
### Actions asynchrones
|
||||||
|
|
||||||
|
- **uploadDocument** : Upload d'un document
|
||||||
|
- **extractDocument** : Extraction des données
|
||||||
|
- **analyzeDocument** : Analyse du document
|
||||||
|
- **getContextData** : Récupération des données contextuelles
|
||||||
|
- **getConseil** : Génération du conseil IA
|
||||||
|
|
||||||
|
## Services API
|
||||||
|
|
||||||
|
### Configuration Axios
|
||||||
|
|
||||||
|
- **Base URL** : Configuration via variables d'environnement
|
||||||
|
- **Timeout** : 60 secondes pour les opérations longues
|
||||||
|
- **Intercepteurs** : Gestion d'erreur et fallback automatique
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
- `POST /api/documents/upload` : Upload de document
|
||||||
|
- `GET /api/documents/{id}/extract` : Extraction de données
|
||||||
|
- `GET /api/documents/{id}/analyze` : Analyse du document
|
||||||
|
- `GET /api/documents/{id}/context` : Données contextuelles
|
||||||
|
- `GET /api/documents/{id}/conseil` : Conseil IA
|
||||||
|
|
||||||
|
### Gestion d'erreur
|
||||||
|
|
||||||
|
- **Intercepteurs** : Détection automatique des erreurs
|
||||||
|
- **Fallback** : Données de démonstration en cas d'erreur
|
||||||
|
- **Codes gérés** : 404, 405, ERR_NETWORK, ERR_CONNECTION_REFUSED
|
||||||
|
|
||||||
|
## Types et interfaces
|
||||||
|
|
||||||
|
### Document
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Document {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
size: number
|
||||||
|
uploadDate: Date
|
||||||
|
status: 'uploading' | 'processing' | 'completed' | 'error'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ExtractionResult
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ExtractionResult {
|
||||||
|
documentId: string
|
||||||
|
text: string
|
||||||
|
language: string
|
||||||
|
documentType: string
|
||||||
|
identities: Identity[]
|
||||||
|
addresses: Address[]
|
||||||
|
properties: Property[]
|
||||||
|
contracts: Contract[]
|
||||||
|
signatures: string[]
|
||||||
|
confidence: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AnalysisResult
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AnalysisResult {
|
||||||
|
documentId: string
|
||||||
|
documentType: string
|
||||||
|
isCNI: boolean
|
||||||
|
credibilityScore: number
|
||||||
|
summary: string
|
||||||
|
recommendations: string[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mode démonstration
|
||||||
|
|
||||||
|
### Fonctionnement
|
||||||
|
|
||||||
|
- **Détection automatique** : Vérification de la disponibilité du backend
|
||||||
|
- **Fallback gracieux** : Basculement vers des données de démonstration
|
||||||
|
- **Données réalistes** : Exemples d'actes notariaux authentiques
|
||||||
|
- **Fonctionnalités complètes** : Toutes les vues opérationnelles
|
||||||
|
|
||||||
|
### Données de démonstration
|
||||||
|
|
||||||
|
- **Documents** : Exemples d'actes de vente, CNI, etc.
|
||||||
|
- **Identités** : Personnes avec informations complètes
|
||||||
|
- **Adresses** : Adresses françaises réalistes
|
||||||
|
- **Biens** : Propriétés avec références cadastrales
|
||||||
|
- **Contrats** : Clauses et montants réalistes
|
||||||
|
|
||||||
|
## Performance et optimisation
|
||||||
|
|
||||||
|
### Code splitting
|
||||||
|
|
||||||
|
- **Lazy loading** : Chargement à la demande des vues
|
||||||
|
- **Suspense** : Gestion des états de chargement
|
||||||
|
- **Bundle optimization** : Optimisation de la taille des bundles
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
- **Redux state** : Mise en cache des données dans le store
|
||||||
|
- **API responses** : Cache des réponses API
|
||||||
|
- **Component memoization** : Optimisation des re-renders
|
||||||
|
|
||||||
|
### Build optimization
|
||||||
|
|
||||||
|
- **Vite** : Build rapide avec optimisations automatiques
|
||||||
|
- **Tree shaking** : Élimination du code mort
|
||||||
|
- **Minification** : Compression du code de production
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
|
||||||
|
- **TypeScript** : Validation de types à la compilation
|
||||||
|
- **Runtime validation** : Validation des données API
|
||||||
|
- **Input sanitization** : Nettoyage des entrées utilisateur
|
||||||
|
|
||||||
|
### CORS et CSP
|
||||||
|
|
||||||
|
- **CORS** : Configuration des en-têtes Cross-Origin
|
||||||
|
- **CSP** : Content Security Policy pour la sécurité
|
||||||
|
- **HTTPS** : Communication sécurisée en production
|
||||||
|
|
||||||
|
## Déploiement
|
||||||
|
|
||||||
|
### Environnements
|
||||||
|
|
||||||
|
- **Développement** : Serveur local avec HMR
|
||||||
|
- **Staging** : Environnement de test
|
||||||
|
- **Production** : Déploiement optimisé
|
||||||
|
|
||||||
|
### Variables d'environnement
|
||||||
|
|
||||||
|
- **VITE_API_URL** : URL de l'API backend
|
||||||
|
- **VITE_*_API_URL** : URLs des APIs externes
|
||||||
|
- **NODE_ENV** : Environnement d'exécution
|
||||||
|
|
||||||
|
### Build de production
|
||||||
|
|
||||||
|
- **Optimisation** : Minification et compression
|
||||||
|
- **Assets** : Optimisation des images et CSS
|
||||||
|
- **Bundle analysis** : Analyse de la taille des bundles
|
||||||
@ -1,32 +0,0 @@
|
|||||||
# Cache des résultats et traitement asynchrone
|
|
||||||
|
|
||||||
## Dossiers utilisés
|
|
||||||
|
|
||||||
- `uploads/<folderHash>`: fichiers déposés (source de vérité)
|
|
||||||
- `cache/<folderHash>`: résultats JSON et flags `.pending` (source pour l’API)
|
|
||||||
- `backend/cache/*`: (désormais vide) ancien emplacement – ne plus utiliser
|
|
||||||
|
|
||||||
## Flux de traitement
|
|
||||||
|
|
||||||
1. Dépôt d’un fichier (`/api/extract`):
|
|
||||||
- Calcule `fileHash` (SHA‑256 du contenu)
|
|
||||||
- Si `cache/<folderHash>/<fileHash>.json` existe: renvoie immédiatement le JSON
|
|
||||||
- Sinon: crée `cache/<folderHash>/<fileHash>.pending`, lance l’OCR/NER, puis écrit le JSON et supprime `.pending`
|
|
||||||
|
|
||||||
2. Listing (`/api/folders/:folderHash/results`):
|
|
||||||
- Agrège tous les JSON présents dans `cache/<folderHash>/`
|
|
||||||
- Pour chaque fichier présent dans `uploads/<folderHash>` sans JSON, crée un flag `.pending` et lance le traitement en arrière‑plan, sans bloquer la réponse
|
|
||||||
|
|
||||||
## Points importants
|
|
||||||
|
|
||||||
- Le traitement images/PDF peut être long; le listing n’attend pas la fin
|
|
||||||
- Le frontal réalise un polling périodique si `hasPending=true`
|
|
||||||
- Les erreurs de traitement suppriment le flag `.pending` et la requête renvoie 500 (extraction) ou 200 avec moins de résultats (listing)
|
|
||||||
|
|
||||||
## Bonnes pratiques
|
|
||||||
|
|
||||||
- N’écrire les résultats que dans `cache/<folderHash>` à la racine
|
|
||||||
- Toujours indexer les résultats par `fileHash.json`
|
|
||||||
- Protéger les accès à `.length` et valeurs potentiellement `undefined` dans le backend
|
|
||||||
|
|
||||||
|
|
||||||
517
docs/DEPLOYMENT.md
Normal file
517
docs/DEPLOYMENT.md
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
# Guide de déploiement - 4NK IA Front Notarial
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Ce guide couvre le déploiement de l'application 4NK IA Front Notarial dans différents environnements.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
### Environnement de développement
|
||||||
|
|
||||||
|
- **Node.js** : >= 22.12.0 (recommandé) ou >= 20.19.0
|
||||||
|
- **npm** : >= 10.0.0
|
||||||
|
- **Git** : Pour la gestion des versions
|
||||||
|
|
||||||
|
### Environnement de production
|
||||||
|
|
||||||
|
- **Serveur web** : Nginx ou Apache
|
||||||
|
- **HTTPS** : Certificat SSL valide
|
||||||
|
- **CDN** : Optionnel pour les assets statiques
|
||||||
|
|
||||||
|
## Configuration des environnements
|
||||||
|
|
||||||
|
### Variables d'environnement
|
||||||
|
|
||||||
|
#### Développement (.env.development)
|
||||||
|
|
||||||
|
```env
|
||||||
|
NODE_ENV=development
|
||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
|
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
|
||||||
|
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||||
|
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
|
||||||
|
VITE_BODACC_API_URL=https://api.bodacc.fr
|
||||||
|
VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Staging (.env.staging)
|
||||||
|
|
||||||
|
```env
|
||||||
|
NODE_ENV=staging
|
||||||
|
VITE_API_URL=https://api-staging.4nkweb.com
|
||||||
|
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
|
||||||
|
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||||
|
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
|
||||||
|
VITE_BODACC_API_URL=https://api.bodacc.fr
|
||||||
|
VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production (.env.production)
|
||||||
|
|
||||||
|
```env
|
||||||
|
NODE_ENV=production
|
||||||
|
VITE_API_URL=https://api.4nkweb.com
|
||||||
|
VITE_CADASTRE_API_URL=https://api.cadastre.gouv.fr
|
||||||
|
VITE_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||||
|
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
|
||||||
|
VITE_BODACC_API_URL=https://api.bodacc.fr
|
||||||
|
VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build de production
|
||||||
|
|
||||||
|
### Script de build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build standard
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Build avec analyse
|
||||||
|
npm run build -- --analyze
|
||||||
|
|
||||||
|
# Build pour un environnement spécifique
|
||||||
|
npm run build -- --mode production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optimisations automatiques
|
||||||
|
|
||||||
|
- **Minification** : Code JavaScript et CSS minifié
|
||||||
|
- **Tree shaking** : Élimination du code mort
|
||||||
|
- **Code splitting** : Division en chunks optimaux
|
||||||
|
- **Asset optimization** : Optimisation des images et fonts
|
||||||
|
|
||||||
|
### Structure du build
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/
|
||||||
|
├── index.html # Point d'entrée HTML
|
||||||
|
├── assets/
|
||||||
|
│ ├── index-[hash].js # Bundle principal
|
||||||
|
│ ├── index-[hash].css # Styles principaux
|
||||||
|
│ ├── [chunk]-[hash].js # Chunks de code splitting
|
||||||
|
│ └── [asset]-[hash].[ext] # Assets optimisés
|
||||||
|
└── favicon.ico # Icône du site
|
||||||
|
```
|
||||||
|
|
||||||
|
## Déploiement sur serveur
|
||||||
|
|
||||||
|
### Configuration Nginx
|
||||||
|
|
||||||
|
#### Fichier de configuration
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name app.4nkweb.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
listen [::]:443 ssl http2;
|
||||||
|
server_name app.4nkweb.com;
|
||||||
|
|
||||||
|
# Certificats SSL
|
||||||
|
ssl_certificate /path/to/certificate.crt;
|
||||||
|
ssl_certificate_key /path/to/private.key;
|
||||||
|
|
||||||
|
# Configuration SSL
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
# HSTS
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||||
|
|
||||||
|
# Root directory
|
||||||
|
root /var/www/4nk-ia-front/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gestion des routes SPA
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache des assets statiques
|
||||||
|
location /assets/ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sécurité
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Apache
|
||||||
|
|
||||||
|
#### Fichier .htaccess
|
||||||
|
|
||||||
|
```apache
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Redirection HTTPS
|
||||||
|
RewriteCond %{HTTPS} off
|
||||||
|
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
|
|
||||||
|
# Gestion des routes SPA
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule . /index.html [L]
|
||||||
|
|
||||||
|
# Cache des assets
|
||||||
|
<FilesMatch "\.(css|js|png|jpg|jpeg|gif|ico|svg)$">
|
||||||
|
ExpiresActive On
|
||||||
|
ExpiresDefault "access plus 1 year"
|
||||||
|
Header set Cache-Control "public, immutable"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
AddOutputFilterByType DEFLATE text/plain
|
||||||
|
AddOutputFilterByType DEFLATE text/html
|
||||||
|
AddOutputFilterByType DEFLATE text/xml
|
||||||
|
AddOutputFilterByType DEFLATE text/css
|
||||||
|
AddOutputFilterByType DEFLATE application/xml
|
||||||
|
AddOutputFilterByType DEFLATE application/xhtml+xml
|
||||||
|
AddOutputFilterByType DEFLATE application/rss+xml
|
||||||
|
AddOutputFilterByType DEFLATE application/javascript
|
||||||
|
AddOutputFilterByType DEFLATE application/x-javascript
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Sécurité
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
Header always set Strict-Transport-Security "max-age=63072000"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Déploiement avec Docker
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Build stage
|
||||||
|
FROM node:22.12-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./ssl:/etc/nginx/ssl:ro
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### Script de déploiement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# deploy.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Building application..."
|
||||||
|
docker build -t 4nk-ia-front .
|
||||||
|
|
||||||
|
echo "Stopping existing container..."
|
||||||
|
docker stop 4nk-ia-front || true
|
||||||
|
docker rm 4nk-ia-front || true
|
||||||
|
|
||||||
|
echo "Starting new container..."
|
||||||
|
docker run -d \
|
||||||
|
--name 4nk-ia-front \
|
||||||
|
-p 80:80 \
|
||||||
|
-p 443:443 \
|
||||||
|
-v /path/to/ssl:/etc/nginx/ssl:ro \
|
||||||
|
4nk-ia-front
|
||||||
|
|
||||||
|
echo "Deployment completed!"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Déploiement avec CI/CD
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [release]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '22.12'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
NODE_ENV: production
|
||||||
|
VITE_API_URL: ${{ secrets.API_URL }}
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@v0.1.5
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.HOST }}
|
||||||
|
username: ${{ secrets.USERNAME }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
cd /var/www/4nk-ia-front
|
||||||
|
git pull origin release
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitLab CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
image: node:22.12-alpine
|
||||||
|
script:
|
||||||
|
- npm ci
|
||||||
|
- npm run test
|
||||||
|
only:
|
||||||
|
- merge_requests
|
||||||
|
- release
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
image: node:22.12-alpine
|
||||||
|
script:
|
||||||
|
- npm ci
|
||||||
|
- npm run build
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- dist/
|
||||||
|
expire_in: 1 hour
|
||||||
|
only:
|
||||||
|
- release
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
stage: deploy
|
||||||
|
image: alpine:latest
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache openssh-client
|
||||||
|
- eval $(ssh-agent -s)
|
||||||
|
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- chmod 700 ~/.ssh
|
||||||
|
script:
|
||||||
|
- scp -r dist/* $DEPLOY_USER@$DEPLOY_HOST:/var/www/4nk-ia-front/
|
||||||
|
- ssh $DEPLOY_USER@$DEPLOY_HOST "sudo systemctl reload nginx"
|
||||||
|
only:
|
||||||
|
- release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring et surveillance
|
||||||
|
|
||||||
|
### Métriques de performance
|
||||||
|
|
||||||
|
- **Temps de chargement** : Mesure du First Contentful Paint
|
||||||
|
- **Taille des bundles** : Surveillance de la taille des assets
|
||||||
|
- **Erreurs JavaScript** : Tracking des erreurs côté client
|
||||||
|
|
||||||
|
### Outils de monitoring
|
||||||
|
|
||||||
|
- **Google Analytics** : Analytics et performance
|
||||||
|
- **Sentry** : Monitoring des erreurs
|
||||||
|
- **Lighthouse** : Audit de performance
|
||||||
|
|
||||||
|
### Configuration Sentry
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "YOUR_SENTRY_DSN",
|
||||||
|
environment: process.env.NODE_ENV,
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback et récupération
|
||||||
|
|
||||||
|
### Stratégie de rollback
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# rollback.sh
|
||||||
|
|
||||||
|
PREVIOUS_VERSION=$1
|
||||||
|
|
||||||
|
if [ -z "$PREVIOUS_VERSION" ]; then
|
||||||
|
echo "Usage: ./rollback.sh <version>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Rolling back to version $PREVIOUS_VERSION..."
|
||||||
|
|
||||||
|
# Restaurer la version précédente
|
||||||
|
git checkout $PREVIOUS_VERSION
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Redéployer
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
|
||||||
|
echo "Rollback completed!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sauvegarde
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# backup.sh
|
||||||
|
|
||||||
|
BACKUP_DIR="/backups/4nk-ia-front"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# Créer le répertoire de sauvegarde
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
|
||||||
|
# Sauvegarder les fichiers
|
||||||
|
tar -czf $BACKUP_DIR/backup_$DATE.tar.gz /var/www/4nk-ia-front
|
||||||
|
|
||||||
|
# Nettoyer les anciennes sauvegardes (garder 7 jours)
|
||||||
|
find $BACKUP_DIR -name "backup_*.tar.gz" -mtime +7 -delete
|
||||||
|
|
||||||
|
echo "Backup completed: backup_$DATE.tar.gz"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sécurité
|
||||||
|
|
||||||
|
### Headers de sécurité
|
||||||
|
|
||||||
|
- **CSP** : Content Security Policy
|
||||||
|
- **HSTS** : HTTP Strict Transport Security
|
||||||
|
- **X-Frame-Options** : Protection contre le clickjacking
|
||||||
|
- **X-Content-Type-Options** : Protection contre MIME sniffing
|
||||||
|
|
||||||
|
### Configuration CSP
|
||||||
|
|
||||||
|
```html
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
|
default-src 'self';
|
||||||
|
script-src 'self' 'unsafe-inline' 'unsafe-eval';
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
img-src 'self' data: https:;
|
||||||
|
connect-src 'self' https://api.4nkweb.com;
|
||||||
|
font-src 'self' data:;
|
||||||
|
">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audit de sécurité
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Audit des dépendances
|
||||||
|
npm audit
|
||||||
|
|
||||||
|
# Audit de sécurité avec Snyk
|
||||||
|
npx snyk test
|
||||||
|
|
||||||
|
# Scan de vulnérabilités
|
||||||
|
npx audit-ci --config audit-ci.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Mise à jour des dépendances
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vérifier les mises à jour
|
||||||
|
npm outdated
|
||||||
|
|
||||||
|
# Mettre à jour les dépendances
|
||||||
|
npm update
|
||||||
|
|
||||||
|
# Mise à jour majeure
|
||||||
|
npx npm-check-updates -u
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nettoyage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nettoyer le cache npm
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
# Nettoyer les node_modules
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Nettoyer le build
|
||||||
|
rm -rf dist
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs et debugging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs Nginx
|
||||||
|
sudo tail -f /var/log/nginx/access.log
|
||||||
|
sudo tail -f /var/log/nginx/error.log
|
||||||
|
|
||||||
|
# Logs de l'application
|
||||||
|
sudo journalctl -u nginx -f
|
||||||
|
|
||||||
|
# Debug des erreurs
|
||||||
|
sudo nginx -t # Test de configuration
|
||||||
|
```
|
||||||
@ -1,178 +0,0 @@
|
|||||||
# 🔐 Système de Hash pour la Gestion des Doublons
|
|
||||||
|
|
||||||
## 📋 **Vue d'ensemble**
|
|
||||||
|
|
||||||
Le système de hash SHA-256 a été implémenté dans le backend 4NK_IA pour éviter les doublons d'upload et optimiser le stockage des fichiers. Ce système garantit qu'un fichier identique ne sera traité qu'une seule fois, même s'il est uploadé plusieurs fois.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 **Fonctionnement**
|
|
||||||
|
|
||||||
### **1. Calcul du Hash**
|
|
||||||
|
|
||||||
- Chaque fichier uploadé est analysé pour calculer son hash SHA-256
|
|
||||||
- Le hash est calculé sur le contenu binaire complet du fichier
|
|
||||||
- Utilisation de la fonction `crypto.createHash('sha256')` de Node.js
|
|
||||||
|
|
||||||
### **2. Détection des Doublons**
|
|
||||||
|
|
||||||
- Avant traitement, le système vérifie si un fichier avec le même hash existe déjà
|
|
||||||
- La fonction `findExistingFileByHash()` parcourt le dossier `uploads/`
|
|
||||||
- Si un doublon est trouvé, le fichier uploadé est supprimé
|
|
||||||
|
|
||||||
### **3. Traitement Optimisé**
|
|
||||||
|
|
||||||
- Le traitement utilise le fichier existant (pas le doublon)
|
|
||||||
- Les résultats d'extraction sont identiques pour les fichiers identiques
|
|
||||||
- Économie de ressources CPU et de stockage
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 **Structure des Fichiers**
|
|
||||||
|
|
||||||
```
|
|
||||||
uploads/
|
|
||||||
├── document-1757980637671.pdf # Fichier original
|
|
||||||
├── document-1757980640576.pdf # Doublon (supprimé après traitement)
|
|
||||||
└── image-1757980651234.jpg # Autre fichier unique
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 **API Endpoints**
|
|
||||||
|
|
||||||
### **GET** `/api/uploads`
|
|
||||||
|
|
||||||
Liste tous les fichiers uploadés avec leurs métadonnées :
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "document-1757980637671.pdf",
|
|
||||||
"size": 1015808,
|
|
||||||
"hash": "a1b2c3d4e5f6789...",
|
|
||||||
"uploadDate": "2025-09-15T23:56:14.751Z",
|
|
||||||
"modifiedDate": "2025-09-15T23:56:14.751Z"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"count": 1,
|
|
||||||
"totalSize": 1015808
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 **Logs et Monitoring**
|
|
||||||
|
|
||||||
### **Logs de Hash**
|
|
||||||
|
|
||||||
```
|
|
||||||
[HASH] Hash du fichier: a1b2c3d4e5f6789...
|
|
||||||
[HASH] Fichier déjà existant trouvé: document-1757980637671.pdf
|
|
||||||
[HASH] Nouveau fichier, traitement normal
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Indicateurs de Performance**
|
|
||||||
|
|
||||||
- **Temps de traitement réduit** pour les doublons
|
|
||||||
- **Stockage optimisé** (pas de fichiers redondants)
|
|
||||||
- **Logs clairs** pour le debugging
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ **Fonctions Techniques**
|
|
||||||
|
|
||||||
### **`calculateFileHash(buffer)`**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function calculateFileHash(buffer) {
|
|
||||||
return crypto.createHash('sha256').update(buffer).digest('hex')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### **`findExistingFileByHash(hash)`**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function findExistingFileByHash(hash) {
|
|
||||||
const uploadDir = 'uploads/'
|
|
||||||
if (!fs.existsSync(uploadDir)) return null
|
|
||||||
|
|
||||||
const files = fs.readdirSync(uploadDir)
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = path.join(uploadDir, file)
|
|
||||||
try {
|
|
||||||
const fileBuffer = fs.readFileSync(filePath)
|
|
||||||
const fileHash = calculateFileHash(fileBuffer)
|
|
||||||
if (fileHash === hash) {
|
|
||||||
return { path: filePath, name: file }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[HASH] Erreur lors de la lecture de ${file}:`, error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ **Avantages**
|
|
||||||
|
|
||||||
1. **Économie de Stockage** : Pas de fichiers dupliqués
|
|
||||||
2. **Performance** : Traitement plus rapide pour les doublons
|
|
||||||
3. **Cohérence** : Résultats identiques pour fichiers identiques
|
|
||||||
4. **Monitoring** : Visibilité sur les fichiers stockés
|
|
||||||
5. **Sécurité** : Hash SHA-256 cryptographiquement sûr
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 **Workflow d'Upload**
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[Fichier Uploadé] --> B[Calcul Hash SHA-256]
|
|
||||||
B --> C{Hash Existe?}
|
|
||||||
C -->|Oui| D[Supprimer Doublon]
|
|
||||||
C -->|Non| E[Conserver Fichier]
|
|
||||||
D --> F[Utiliser Fichier Existant]
|
|
||||||
E --> G[Traitement Normal]
|
|
||||||
F --> H[Extraction & NER]
|
|
||||||
G --> H
|
|
||||||
H --> I[Résultat JSON]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 **Tests**
|
|
||||||
|
|
||||||
### **Test de Doublon**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Premier upload
|
|
||||||
curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract
|
|
||||||
|
|
||||||
# Deuxième upload du même fichier
|
|
||||||
curl -X POST -F "document=@test.pdf" http://localhost:3001/api/extract
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Vérification**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Lister les fichiers uploadés
|
|
||||||
curl http://localhost:3001/api/uploads
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 **Notes d'Implémentation**
|
|
||||||
|
|
||||||
- **Algorithme** : SHA-256 (256 bits)
|
|
||||||
- **Performance** : O(n) pour la recherche de doublons
|
|
||||||
- **Stockage** : Hash stocké en hexadécimal (64 caractères)
|
|
||||||
- **Compatibilité** : Fonctionne avec tous les types de fichiers supportés
|
|
||||||
- **Nettoyage** : Suppression automatique des doublons temporaires
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_Documentation mise à jour le 15 septembre 2025_
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
# 🎉 Système IA - Fonctionnel et Opérationnel
|
|
||||||
|
|
||||||
## ✅ **Statut : SYSTÈME FONCTIONNEL**
|
|
||||||
|
|
||||||
Le système IA est maintenant **entièrement fonctionnel** et accessible via HTTPS sur le domaine `ia.4nkweb.com`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 **Accès au Système**
|
|
||||||
|
|
||||||
### **URL de Production**
|
|
||||||
- **Frontend** : https://ia.4nkweb.com
|
|
||||||
- **API Backend** : https://ia.4nkweb.com/api/
|
|
||||||
- **Health Check** : https://ia.4nkweb.com/api/health
|
|
||||||
|
|
||||||
### **Certificat SSL**
|
|
||||||
- ✅ **HTTPS activé** avec Let's Encrypt
|
|
||||||
- ✅ **Renouvellement automatique** configuré
|
|
||||||
- ✅ **Redirection HTTP → HTTPS** active
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 **Données de Test Disponibles**
|
|
||||||
|
|
||||||
Le système contient actuellement **3 documents de test** :
|
|
||||||
|
|
||||||
### **1. Contrat de Vente (PDF)**
|
|
||||||
- **Fichier** : `contrat_vente.pdf`
|
|
||||||
- **Entités extraites** :
|
|
||||||
- **Personnes** : Jean Dupont, Marie Martin
|
|
||||||
- **Adresses** : 123 rue de la Paix (75001), 456 avenue des Champs (75008), 789 boulevard Saint-Germain (75006)
|
|
||||||
- **Propriétés** : 789 boulevard Saint-Germain, 75006 Paris
|
|
||||||
- **Contrats** : Contrat de vente
|
|
||||||
|
|
||||||
### **2. Carte d'Identité (Image)**
|
|
||||||
- **Fichier** : `cni_jean_dupont.jpg`
|
|
||||||
- **Entités extraites** :
|
|
||||||
- **Personnes** : Jean Dupont
|
|
||||||
- **Adresses** : 123 rue de la Paix, 75001 Paris
|
|
||||||
|
|
||||||
### **3. Document de Test (Texte)**
|
|
||||||
- **Fichier** : `test_sync.txt`
|
|
||||||
- **Entités extraites** :
|
|
||||||
- **Personnes** : Test User
|
|
||||||
- **Adresses** : 456 Test Avenue
|
|
||||||
- **Entreprises** : Test Corp
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ **Architecture Technique**
|
|
||||||
|
|
||||||
### **Frontend (React + TypeScript)**
|
|
||||||
- **Framework** : React 19 + TypeScript
|
|
||||||
- **Build** : Vite 7
|
|
||||||
- **UI** : Material-UI (MUI) v7
|
|
||||||
- **État** : Redux Toolkit
|
|
||||||
- **Routing** : React Router v7
|
|
||||||
- **Port** : 5174 (dev) / Nginx (prod)
|
|
||||||
|
|
||||||
### **Backend (Node.js + Express)**
|
|
||||||
- **Framework** : Express.js
|
|
||||||
- **OCR** : Tesseract.js
|
|
||||||
- **NER** : Règles personnalisées
|
|
||||||
- **Port** : 3001
|
|
||||||
- **Dossiers** : `uploads/` et `cache/`
|
|
||||||
|
|
||||||
### **Proxy (Nginx)**
|
|
||||||
- **Configuration** : `/etc/nginx/conf.d/ia.4nkweb.com.conf`
|
|
||||||
- **SSL** : Let's Encrypt
|
|
||||||
- **Proxy** : `/api/` → Backend (127.0.0.1:3001)
|
|
||||||
- **Static** : `/` → Frontend (`dist/`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔄 **Flux de Traitement des Documents**
|
|
||||||
|
|
||||||
### **1. Détection des Fichiers**
|
|
||||||
```
|
|
||||||
uploads/{folderHash}/ → Détection automatique
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Traitement Synchrone**
|
|
||||||
```
|
|
||||||
Fichier détecté → processDocument() → Cache JSON
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3. Extraction des Entités**
|
|
||||||
- **OCR** : Tesseract.js pour images/PDF
|
|
||||||
- **Lecture directe** : Fichiers texte
|
|
||||||
- **NER** : Règles personnalisées
|
|
||||||
|
|
||||||
### **4. Stockage des Résultats**
|
|
||||||
```
|
|
||||||
cache/{folderHash}/{fileHash}.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### **5. API Response**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"folderHash": "7d99a85daf66a0081a0e881630e6b39b",
|
|
||||||
"results": [...],
|
|
||||||
"pending": [],
|
|
||||||
"hasPending": false,
|
|
||||||
"count": 3
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ **Commandes de Gestion**
|
|
||||||
|
|
||||||
### **Démarrer le Backend**
|
|
||||||
```bash
|
|
||||||
cd /home/debian/4NK_IA_front/backend
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
|
||||||
node server.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Rebuilder le Frontend**
|
|
||||||
```bash
|
|
||||||
cd /home/debian/4NK_IA_front
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Vérifier les Services**
|
|
||||||
```bash
|
|
||||||
# Backend
|
|
||||||
curl -s https://ia.4nkweb.com/api/health
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
curl -sI https://ia.4nkweb.com/
|
|
||||||
|
|
||||||
# Documents
|
|
||||||
curl -s https://ia.4nkweb.com/api/folders/7d99a85daf66a0081a0e881630e6b39b/results
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 **Structure des Dossiers**
|
|
||||||
|
|
||||||
```
|
|
||||||
/home/debian/4NK_IA_front/
|
|
||||||
├── backend/
|
|
||||||
│ ├── server.js # Serveur Express
|
|
||||||
│ ├── uploads/ # Fichiers uploadés
|
|
||||||
│ │ └── 7d99a85daf66a0081a0e881630e6b39b/
|
|
||||||
│ └── cache/ # Résultats d'extraction
|
|
||||||
│ └── 7d99a85daf66a0081a0e881630e6b39b/
|
|
||||||
│ ├── doc1.json
|
|
||||||
│ ├── doc2.json
|
|
||||||
│ └── test_sync.json
|
|
||||||
├── dist/ # Build frontend
|
|
||||||
├── src/ # Code source frontend
|
|
||||||
└── docs/ # Documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 **Corrections Apportées**
|
|
||||||
|
|
||||||
### **1. Problème Mixed Content**
|
|
||||||
- **Avant** : `http://172.17.222.203:3001/api`
|
|
||||||
- **Après** : `/api` (proxy HTTPS)
|
|
||||||
|
|
||||||
### **2. Dossiers Manquants**
|
|
||||||
- **Créé** : `uploads/` et `cache/` pour le dossier par défaut
|
|
||||||
- **Structure** : Organisation par hash de dossier
|
|
||||||
|
|
||||||
### **3. Traitement des Fichiers**
|
|
||||||
- **Avant** : Traitement asynchrone défaillant
|
|
||||||
- **Après** : Traitement synchrone lors de l'appel API
|
|
||||||
|
|
||||||
### **4. Support des Fichiers Texte**
|
|
||||||
- **Ajouté** : Lecture directe des fichiers `.txt`
|
|
||||||
- **OCR** : Réservé aux images et PDF
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 **Fonctionnalités Opérationnelles**
|
|
||||||
|
|
||||||
### ✅ **Upload de Documents**
|
|
||||||
- Support multi-format (PDF, JPEG, PNG, TIFF, TXT)
|
|
||||||
- Validation des types MIME
|
|
||||||
- Gestion des doublons par hash
|
|
||||||
|
|
||||||
### ✅ **Extraction OCR**
|
|
||||||
- Tesseract.js pour images
|
|
||||||
- pdf-parse pour PDF
|
|
||||||
- Lecture directe pour texte
|
|
||||||
|
|
||||||
### ✅ **Reconnaissance d'Entités**
|
|
||||||
- Personnes (noms, prénoms)
|
|
||||||
- Adresses (complètes)
|
|
||||||
- Entreprises
|
|
||||||
- Propriétés
|
|
||||||
- Contrats
|
|
||||||
|
|
||||||
### ✅ **Interface Utilisateur**
|
|
||||||
- React + Material-UI
|
|
||||||
- Navigation entre documents
|
|
||||||
- Affichage des résultats d'extraction
|
|
||||||
- Gestion des dossiers
|
|
||||||
|
|
||||||
### ✅ **API REST**
|
|
||||||
- Endpoints complets
|
|
||||||
- Format JSON standardisé
|
|
||||||
- Gestion d'erreurs
|
|
||||||
- Health checks
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 **Prochaines Étapes Recommandées**
|
|
||||||
|
|
||||||
### **1. Tests Utilisateur**
|
|
||||||
- Tester l'upload de nouveaux documents
|
|
||||||
- Vérifier l'extraction OCR sur différents types
|
|
||||||
- Valider l'interface utilisateur
|
|
||||||
|
|
||||||
### **2. Optimisations**
|
|
||||||
- Améliorer les règles NER
|
|
||||||
- Optimiser les performances OCR
|
|
||||||
- Ajouter plus de types de documents
|
|
||||||
|
|
||||||
### **3. Monitoring**
|
|
||||||
- Logs détaillés
|
|
||||||
- Métriques de performance
|
|
||||||
- Alertes de santé
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 **Support Technique**
|
|
||||||
|
|
||||||
Le système est maintenant **entièrement fonctionnel** et prêt pour la production. Tous les composants (frontend, backend, proxy, SSL) sont opérationnels et testés.
|
|
||||||
|
|
||||||
**Accès immédiat** : https://ia.4nkweb.com
|
|
||||||
627
docs/TESTING.md
Normal file
627
docs/TESTING.md
Normal file
@ -0,0 +1,627 @@
|
|||||||
|
# Guide de tests - 4NK IA Front Notarial
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
Ce guide couvre la stratégie de tests pour l'application 4NK IA Front Notarial, incluant les tests unitaires,
|
||||||
|
d'intégration et end-to-end.
|
||||||
|
|
||||||
|
## Stack de test
|
||||||
|
|
||||||
|
### Outils principaux
|
||||||
|
|
||||||
|
- **Vitest** : Framework de test rapide et moderne
|
||||||
|
- **Testing Library** : Tests d'intégration React
|
||||||
|
- **JSDOM** : Environnement DOM simulé
|
||||||
|
- **MSW** : Mock Service Worker pour les APIs
|
||||||
|
- **Coverage V8** : Rapport de couverture de code
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### vitest.config.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
globals: true,
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'dist/',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/*.config.*',
|
||||||
|
'**/setup.*'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### setup.test.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { expect, afterEach } from 'vitest'
|
||||||
|
import { cleanup } from '@testing-library/react'
|
||||||
|
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||||
|
|
||||||
|
// Étendre les matchers de testing-library
|
||||||
|
expect.extend(matchers)
|
||||||
|
|
||||||
|
// Nettoyer après chaque test
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types de tests
|
||||||
|
|
||||||
|
### Tests unitaires
|
||||||
|
|
||||||
|
#### Composants React
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/components/Layout.test.tsx
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { store } from '../../src/store'
|
||||||
|
import { Layout } from '../../src/components/Layout'
|
||||||
|
|
||||||
|
const renderWithProviders = (ui: React.ReactElement) => {
|
||||||
|
return render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
{ui}
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Layout', () => {
|
||||||
|
it('should render the application title', () => {
|
||||||
|
renderWithProviders(<Layout><div>Test content</div></Layout>)
|
||||||
|
|
||||||
|
expect(screen.getByText('4NK IA - Front Notarial')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render navigation tabs', () => {
|
||||||
|
renderWithProviders(<Layout><div>Test content</div></Layout>)
|
||||||
|
|
||||||
|
expect(screen.getByRole('tablist')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Services API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/services/api.test.ts
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { documentApi } from '../../src/services/api'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// Mock axios
|
||||||
|
vi.mock('axios')
|
||||||
|
const mockedAxios = vi.mocked(axios)
|
||||||
|
|
||||||
|
describe('documentApi', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('upload', () => {
|
||||||
|
it('should upload a document successfully', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 'doc_123',
|
||||||
|
name: 'test.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
size: 1024,
|
||||||
|
uploadDate: new Date(),
|
||||||
|
status: 'completed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mockedAxios.create.mockReturnValue({
|
||||||
|
post: vi.fn().mockResolvedValue(mockResponse)
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||||
|
const result = await documentApi.upload(file)
|
||||||
|
|
||||||
|
expect(result.id).toBe('doc_123')
|
||||||
|
expect(result.name).toBe('test.pdf')
|
||||||
|
expect(result.status).toBe('completed')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return demo data on error', async () => {
|
||||||
|
mockedAxios.create.mockReturnValue({
|
||||||
|
post: vi.fn().mockRejectedValue(new Error('Network error'))
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||||
|
const result = await documentApi.upload(file)
|
||||||
|
|
||||||
|
expect(result.id).toMatch(/^demo-/)
|
||||||
|
expect(result.name).toBe('test.pdf')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Redux Slices
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/store/documentSlice.test.ts
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import documentReducer, { setCurrentDocument } from '../../src/store/documentSlice'
|
||||||
|
import { uploadDocument } from '../../src/store/documentSlice'
|
||||||
|
|
||||||
|
describe('documentSlice', () => {
|
||||||
|
const initialState = {
|
||||||
|
documents: [],
|
||||||
|
currentDocument: null,
|
||||||
|
extractionResult: null,
|
||||||
|
analysisResult: null,
|
||||||
|
contextResult: null,
|
||||||
|
conseilResult: null,
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle setCurrentDocument', () => {
|
||||||
|
const document = {
|
||||||
|
id: 'doc_123',
|
||||||
|
name: 'test.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
size: 1024,
|
||||||
|
uploadDate: new Date(),
|
||||||
|
status: 'completed' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = setCurrentDocument(document)
|
||||||
|
const newState = documentReducer(initialState, action)
|
||||||
|
|
||||||
|
expect(newState.currentDocument).toEqual(document)
|
||||||
|
expect(newState.extractionResult).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle uploadDocument.pending', () => {
|
||||||
|
const action = { type: uploadDocument.pending.type }
|
||||||
|
const newState = documentReducer(initialState, action)
|
||||||
|
|
||||||
|
expect(newState.loading).toBe(true)
|
||||||
|
expect(newState.error).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests d'intégration
|
||||||
|
|
||||||
|
#### Vues complètes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/views/UploadView.test.tsx
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { store } from '../../src/store'
|
||||||
|
import { UploadView } from '../../src/views/UploadView'
|
||||||
|
|
||||||
|
const renderWithProviders = (ui: React.ReactElement) => {
|
||||||
|
return render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
{ui}
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('UploadView', () => {
|
||||||
|
it('should render upload area', () => {
|
||||||
|
renderWithProviders(<UploadView />)
|
||||||
|
|
||||||
|
expect(screen.getByText(/glisser-déposer/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle file upload', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderWithProviders(<UploadView />)
|
||||||
|
|
||||||
|
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||||
|
const input = screen.getByLabelText(/sélectionner des fichiers/i)
|
||||||
|
|
||||||
|
await user.upload(input, file)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('test.pdf')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display document list after upload', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderWithProviders(<UploadView />)
|
||||||
|
|
||||||
|
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
|
||||||
|
const input = screen.getByLabelText(/sélectionner des fichiers/i)
|
||||||
|
|
||||||
|
await user.upload(input, file)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('list')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('test.pdf')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Navigation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/navigation.test.tsx
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { store } from '../src/store'
|
||||||
|
import App from '../src/App'
|
||||||
|
|
||||||
|
const renderWithProviders = (ui: React.ReactElement) => {
|
||||||
|
return render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
{ui}
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Navigation', () => {
|
||||||
|
it('should navigate between tabs', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderWithProviders(<App />)
|
||||||
|
|
||||||
|
// Vérifier que l'onglet Upload est actif par défaut
|
||||||
|
expect(screen.getByText('Upload')).toHaveAttribute('aria-selected', 'true')
|
||||||
|
|
||||||
|
// Cliquer sur l'onglet Extraction
|
||||||
|
await user.click(screen.getByText('Extraction'))
|
||||||
|
|
||||||
|
expect(screen.getByText('Extraction')).toHaveAttribute('aria-selected', 'true')
|
||||||
|
expect(screen.getByText('Upload')).toHaveAttribute('aria-selected', 'false')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests d'API avec MSW
|
||||||
|
|
||||||
|
#### Configuration MSW
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/mocks/handlers.ts
|
||||||
|
import { http, HttpResponse } from 'msw'
|
||||||
|
|
||||||
|
export const handlers = [
|
||||||
|
// Upload de document
|
||||||
|
http.post('/api/documents/upload', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
id: 'doc_123',
|
||||||
|
name: 'test.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
size: 1024,
|
||||||
|
uploadDate: new Date().toISOString(),
|
||||||
|
status: 'completed'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Extraction de données
|
||||||
|
http.get('/api/documents/:id/extract', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
documentId: 'doc_123',
|
||||||
|
text: 'Texte extrait du document...',
|
||||||
|
language: 'fr',
|
||||||
|
documentType: 'Acte de vente',
|
||||||
|
identities: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'person',
|
||||||
|
firstName: 'Jean',
|
||||||
|
lastName: 'Dupont',
|
||||||
|
birthDate: '1980-05-15',
|
||||||
|
nationality: 'Française',
|
||||||
|
confidence: 0.95
|
||||||
|
}
|
||||||
|
],
|
||||||
|
addresses: [],
|
||||||
|
properties: [],
|
||||||
|
contracts: [],
|
||||||
|
signatures: [],
|
||||||
|
confidence: 0.92
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Erreur de connexion
|
||||||
|
http.get('/api/documents/:id/analyze', () => {
|
||||||
|
return HttpResponse.error()
|
||||||
|
})
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tests avec MSW
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/integration/api.test.tsx
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
import { handlers } from '../mocks/handlers'
|
||||||
|
import { documentApi } from '../../src/services/api'
|
||||||
|
|
||||||
|
const server = setupServer(...handlers)
|
||||||
|
|
||||||
|
beforeAll(() => server.listen())
|
||||||
|
afterEach(() => server.resetHandlers())
|
||||||
|
afterAll(() => server.close())
|
||||||
|
|
||||||
|
describe('API Integration', () => {
|
||||||
|
it('should extract document data', async () => {
|
||||||
|
const result = await documentApi.extract('doc_123')
|
||||||
|
|
||||||
|
expect(result.documentId).toBe('doc_123')
|
||||||
|
expect(result.identities).toHaveLength(1)
|
||||||
|
expect(result.identities[0].firstName).toBe('Jean')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle API errors gracefully', async () => {
|
||||||
|
const result = await documentApi.analyze('doc_123')
|
||||||
|
|
||||||
|
// Devrait retourner des données de démonstration
|
||||||
|
expect(result.documentId).toBe('doc_123')
|
||||||
|
expect(result.credibilityScore).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests de performance
|
||||||
|
|
||||||
|
### Tests de rendu
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/performance/render.test.tsx
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { store } from '../../src/store'
|
||||||
|
import App from '../../src/App'
|
||||||
|
|
||||||
|
describe('Performance', () => {
|
||||||
|
it('should render app within acceptable time', () => {
|
||||||
|
const start = performance.now()
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
|
||||||
|
const end = performance.now()
|
||||||
|
const renderTime = end - start
|
||||||
|
|
||||||
|
// Le rendu initial devrait prendre moins de 100ms
|
||||||
|
expect(renderTime).toBeLessThan(100)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests de mémoire
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/performance/memory.test.tsx
|
||||||
|
import { render, cleanup } from '@testing-library/react'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { store } from '../../src/store'
|
||||||
|
import { UploadView } from '../../src/views/UploadView'
|
||||||
|
|
||||||
|
describe('Memory Management', () => {
|
||||||
|
it('should not leak memory on multiple renders', () => {
|
||||||
|
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0
|
||||||
|
|
||||||
|
// Rendre et nettoyer plusieurs fois
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<UploadView />
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalMemory = (performance as any).memory?.usedJSHeapSize || 0
|
||||||
|
const memoryIncrease = finalMemory - initialMemory
|
||||||
|
|
||||||
|
// L'augmentation de mémoire devrait être raisonnable
|
||||||
|
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024) // 10MB
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests d'accessibilité
|
||||||
|
|
||||||
|
### Tests avec jest-axe
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tests/accessibility/a11y.test.tsx
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { axe, toHaveNoViolations } from 'jest-axe'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { store } from '../../src/store'
|
||||||
|
import { Layout } from '../../src/components/Layout'
|
||||||
|
|
||||||
|
expect.extend(toHaveNoViolations)
|
||||||
|
|
||||||
|
describe('Accessibility', () => {
|
||||||
|
it('should not have accessibility violations', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Layout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</Layout>
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
|
||||||
|
const results = await axe(container)
|
||||||
|
expect(results).toHaveNoViolations()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have proper ARIA labels', () => {
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Layout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</Layout>
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByRole('banner')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('tablist')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts de test
|
||||||
|
|
||||||
|
### package.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
|
"test:debug": "vitest --inspect-brk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exécution des tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tests en mode watch
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Tests avec interface graphique
|
||||||
|
npm run test:ui
|
||||||
|
|
||||||
|
# Tests une seule fois
|
||||||
|
npm run test:run
|
||||||
|
|
||||||
|
# Tests avec couverture
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# Tests en mode debug
|
||||||
|
npm run test:debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration CI/CD
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '22.12'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test:coverage
|
||||||
|
|
||||||
|
- name: Upload coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
file: ./coverage/lcov.info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bonnes pratiques
|
||||||
|
|
||||||
|
### Organisation des tests
|
||||||
|
|
||||||
|
- **Un fichier de test par composant** : `Component.test.tsx`
|
||||||
|
- **Tests groupés par fonctionnalité** : `describe` blocks
|
||||||
|
- **Tests isolés** : Chaque test doit être indépendant
|
||||||
|
- **Noms descriptifs** : `it('should do something specific')`
|
||||||
|
|
||||||
|
### Mocking
|
||||||
|
|
||||||
|
- **Mock des dépendances externes** : APIs, services
|
||||||
|
- **Mock des hooks** : React hooks personnalisés
|
||||||
|
- **Mock des modules** : Modules Node.js
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
|
||||||
|
- **Assertions spécifiques** : Éviter les assertions génériques
|
||||||
|
- **Tests de régression** : Vérifier les bugs corrigés
|
||||||
|
- **Tests de cas limites** : Valeurs nulles, erreurs
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Tests rapides** : Éviter les tests lents
|
||||||
|
- **Parallélisation** : Utiliser `--threads` pour Vitest
|
||||||
|
- **Cache** : Utiliser le cache des dépendances
|
||||||
|
|
||||||
|
## Métriques de qualité
|
||||||
|
|
||||||
|
### Couverture de code
|
||||||
|
|
||||||
|
- **Minimum 80%** : Couverture globale
|
||||||
|
- **Minimum 90%** : Composants critiques
|
||||||
|
- **100%** : Fonctions utilitaires
|
||||||
|
|
||||||
|
### Types de couverture
|
||||||
|
|
||||||
|
- **Statements** : Instructions exécutées
|
||||||
|
- **Branches** : Branches conditionnelles
|
||||||
|
- **Functions** : Fonctions appelées
|
||||||
|
- **Lines** : Lignes de code exécutées
|
||||||
|
|
||||||
|
### Rapport de couverture
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Générer le rapport
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# Ouvrir le rapport HTML
|
||||||
|
open coverage/index.html
|
||||||
|
```
|
||||||
@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
title: Stratégie de tests (shell)
|
|
||||||
---
|
|
||||||
|
|
||||||
# Objectif
|
|
||||||
|
|
||||||
Fournir des tests shell simples, paramétrables par variables d’environnement, pour valider les fonctionnalités clés sans dépendre d’outils lourds.
|
|
||||||
|
|
||||||
# Pré-requis
|
|
||||||
|
|
||||||
- Backend accessible sur `http://localhost:3001`
|
|
||||||
- PM2 (optionnel) pour relancer le backend
|
|
||||||
|
|
||||||
# Tests disponibles
|
|
||||||
|
|
||||||
1) Upload volumineux (50 Mo): `tests/upload_100mb.test.sh`
|
|
||||||
- Valide l’absence d’erreur 413 côté proxy et Multer
|
|
||||||
|
|
||||||
2) OCR CNI (paramétrable): `tests/ocr_cni_pipeline.test.sh`
|
|
||||||
- Variables requises:
|
|
||||||
- `SAMPLE_CNI` (chemin vers une image CNI)
|
|
||||||
- `FOLDER_HASH` (hash du dossier cible)
|
|
||||||
- Skips si variables non définies
|
|
||||||
|
|
||||||
3) Enrichissement Adresse (paramétrable): `tests/enrich_address_pipeline.test.sh`
|
|
||||||
- Variables requises:
|
|
||||||
- `FOLDER_HASH`, `FILE_HASH` (doit référencer un document déjà extrait avec au moins une adresse)
|
|
||||||
- Skips si variables non définies
|
|
||||||
|
|
||||||
# Exécution
|
|
||||||
|
|
||||||
```
|
|
||||||
chmod +x tests/*.sh
|
|
||||||
./tests/upload_100mb.test.sh
|
|
||||||
SAMPLE_CNI=/chemin/cni.jpg FOLDER_HASH=default ./tests/ocr_cni_pipeline.test.sh
|
|
||||||
FOLDER_HASH=xxxx FILE_HASH=yyyy ./tests/enrich_address_pipeline.test.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
# Interprétation
|
|
||||||
|
|
||||||
- OK: test validé
|
|
||||||
- SKIP: conditions non remplies (variables/env ou données manquantes)
|
|
||||||
- ERR: action attendue non réalisée
|
|
||||||
35
docs/TODO.md
35
docs/TODO.md
@ -1,4 +1,4 @@
|
|||||||
# Spécifications fonctionnelles duLecoffre.io
|
# Spécifications fonctionnelles du front notarial
|
||||||
|
|
||||||
on veut crer un front pour les notaires et leurs assistants afin de :
|
on veut crer un front pour les notaires et leurs assistants afin de :
|
||||||
les notaires vont l'utiliser dans le cadre de leur processus métiers et des types d'actes.
|
les notaires vont l'utiliser dans le cadre de leur processus métiers et des types d'actes.
|
||||||
@ -9,7 +9,7 @@ faire une api et une une ihm qui les consomme pour:
|
|||||||
|
|
||||||
1. Détecter un type de document
|
1. Détecter un type de document
|
||||||
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
|
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
|
||||||
acheteur, héritiers.... propres aux actes notariés
|
acheteur, héritiers.... propres aux actes notariés
|
||||||
3. Si c'est une CNI, définir le pays
|
3. Si c'est une CNI, définir le pays
|
||||||
4. Pour les identité : rechercher des informations générales sur la personne
|
4. Pour les identité : rechercher des informations générales sur la personne
|
||||||
5. Pour les adresses vérifier:
|
5. Pour les adresses vérifier:
|
||||||
@ -47,9 +47,9 @@ Retrait gonflement des argiles Opérations sur les retrait de gonflement des arg
|
|||||||
Opérations sur le Détail des risques SSP
|
Opérations sur le Détail des risques SSP
|
||||||
Sites et sols pollués (SSP) TIM
|
Sites et sols pollués (SSP) TIM
|
||||||
Opérations sur les Transmissions d'Informations au Maire (TIM)
|
Opérations sur les Transmissions d'Informations au Maire (TIM)
|
||||||
TRI Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
TRI Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||||
TRI - Zonage réglementaire
|
TRI - Zonage réglementaire
|
||||||
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||||
Zonage Sismique
|
Zonage Sismique
|
||||||
Opérations sur le risque sismique
|
Opérations sur le risque sismique
|
||||||
Géoportail urba
|
Géoportail urba
|
||||||
@ -65,11 +65,12 @@ Vigilances DOW
|
|||||||
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
|
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
|
||||||
Infogreffe
|
Infogreffe
|
||||||
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
|
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
|
||||||
RBE (<EFBFBD> coupler avec infogreffe ci-dessus)
|
RBE (Ã coupler avec infogreffe ci-dessus)
|
||||||
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
|
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
|
||||||
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
||||||
joindre le PDF suivant complété :
|
joindre le PDF suivant complété :
|
||||||
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
|
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf)
|
||||||
|
6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
|
||||||
|
|
||||||
L'écran s'affichera depuis un bouton sur /home/desk/code/lecoffre_front
|
L'écran s'affichera depuis un bouton sur /home/desk/code/lecoffre_front
|
||||||
mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images
|
mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images
|
||||||
@ -95,7 +96,7 @@ faire une api et une une ihm qui les consomme pour:
|
|||||||
|
|
||||||
1. Détecter un type de document
|
1. Détecter un type de document
|
||||||
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
|
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
|
||||||
acheteur, héritiers.... propres aux actes notariés
|
acheteur, héritiers.... propres aux actes notariés
|
||||||
3. Si c'est une CNI, définir le pays
|
3. Si c'est une CNI, définir le pays
|
||||||
4. Pour les identité : rechercher des informations générales sur la personne
|
4. Pour les identité : rechercher des informations générales sur la personne
|
||||||
5. Pour les adresses vérifier:
|
5. Pour les adresses vérifier:
|
||||||
@ -133,9 +134,9 @@ Retrait gonflement des argiles Opérations sur les retrait de gonflement des arg
|
|||||||
Opérations sur le Détail des risques SSP
|
Opérations sur le Détail des risques SSP
|
||||||
Sites et sols pollués (SSP) TIM
|
Sites et sols pollués (SSP) TIM
|
||||||
Opérations sur les Transmissions d'Informations au Maire (TIM)
|
Opérations sur les Transmissions d'Informations au Maire (TIM)
|
||||||
TRI Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
TRI Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||||
TRI - Zonage réglementaire
|
TRI - Zonage réglementaire
|
||||||
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||||
Zonage Sismique
|
Zonage Sismique
|
||||||
Opérations sur le risque sismique
|
Opérations sur le risque sismique
|
||||||
Géoportail urba
|
Géoportail urba
|
||||||
@ -151,11 +152,12 @@ Vigilances DOW
|
|||||||
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
|
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
|
||||||
Infogreffe
|
Infogreffe
|
||||||
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
|
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
|
||||||
RBE (<EFBFBD> coupler avec infogreffe ci-dessus)
|
RBE (Ã coupler avec infogreffe ci-dessus)
|
||||||
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
|
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
|
||||||
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
||||||
joindre le PDF suivant complété :
|
joindre le PDF suivant complété :
|
||||||
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf<64>](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf<64>) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
|
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdfÂ](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdfÂ)
|
||||||
|
6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
|
||||||
on veut crer un front pour les notaires et leurs assistants afin de :
|
on veut crer un front pour les notaires et leurs assistants afin de :
|
||||||
les notaires vont l'utiliser dans le cadre de leur processus métiers et des types d'actes.
|
les notaires vont l'utiliser dans le cadre de leur processus métiers et des types d'actes.
|
||||||
faire une API et une IHM pour l'OCR, la catégorisation, la vraissemblance et la recherche d'information des
|
faire une API et une IHM pour l'OCR, la catégorisation, la vraissemblance et la recherche d'information des
|
||||||
@ -165,7 +167,7 @@ faire une api et une une ihm qui les consomme pour:
|
|||||||
|
|
||||||
1. Détecter un type de document
|
1. Détecter un type de document
|
||||||
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
|
2. Extraire tout le texte, définir des objets standard identités, lieux, biens, contrats, communes, vendeur,
|
||||||
acheteur, héritiers.... propres aux actes notariés
|
acheteur, héritiers.... propres aux actes notariés
|
||||||
3. Si c'est une CNI, définir le pays
|
3. Si c'est une CNI, définir le pays
|
||||||
4. Pour les identité : rechercher des informations générales sur la personne
|
4. Pour les identité : rechercher des informations générales sur la personne
|
||||||
5. Pour les adresses vérifier:
|
5. Pour les adresses vérifier:
|
||||||
@ -203,9 +205,9 @@ Retrait gonflement des argiles Opérations sur les retrait de gonflement des arg
|
|||||||
Opérations sur le Détail des risques SSP
|
Opérations sur le Détail des risques SSP
|
||||||
Sites et sols pollués (SSP) TIM
|
Sites et sols pollués (SSP) TIM
|
||||||
Opérations sur les Transmissions d'Informations au Maire (TIM)
|
Opérations sur les Transmissions d'Informations au Maire (TIM)
|
||||||
TRI Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
TRI Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||||
TRI - Zonage réglementaire
|
TRI - Zonage réglementaire
|
||||||
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||||
Zonage Sismique
|
Zonage Sismique
|
||||||
Opérations sur le risque sismique
|
Opérations sur le risque sismique
|
||||||
Géoportail urba
|
Géoportail urba
|
||||||
@ -221,11 +223,12 @@ Vigilances DOW
|
|||||||
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
|
JONES[https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/](https://www.dowjones.com/business-intelligence/risk/products/data-feeds-apis/)
|
||||||
Infogreffe
|
Infogreffe
|
||||||
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
|
[https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details](https://entreprise.api.gouv.fr/catalogue/infogreffe/rcs/extrait#parameters_details)
|
||||||
RBE (<EFBFBD> coupler avec infogreffe ci-dessus)
|
RBE (Ã coupler avec infogreffe ci-dessus)
|
||||||
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
|
[https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/](https://www.data.gouv.fr/dataservices/api-registre-des-beneficiaires-effectifs-rbe/)
|
||||||
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
||||||
joindre le PDF suivant complété :
|
joindre le PDF suivant complété :
|
||||||
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf) 6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
|
[https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf](https://www.inpi.fr/sites/default/files/2025-01/Formulaire_demande_acces_BE.pdf)
|
||||||
|
6) donner un score de vraissemblance sur le document 7) donner une avis de synthèse sur le document
|
||||||
|
|
||||||
L'écran s'affichera depuis un bouton sur /home/desk/code/lecoffre_front
|
L'écran s'affichera depuis un bouton sur /home/desk/code/lecoffre_front
|
||||||
mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images
|
mais il faudra pouvoir ajouter/glisser un document en 1 ou plusieurs document uploadés et voir les previews images
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
## Annuaire de noms (FR et autres)
|
|
||||||
|
|
||||||
- Emplacement: `backend/data/names/`
|
|
||||||
- Fichiers CSV/TSV/texte séparés par `,` `;` ou `\t`.
|
|
||||||
- Noms de fichiers:
|
|
||||||
- `firstnames_fr.csv`, `firstnames_en.csv`, `prenoms_fr.csv`, etc.
|
|
||||||
- `lastnames_fr.csv`, `surnames_en.csv`, `noms_fr.csv`, etc.
|
|
||||||
- Chargement: automatique au démarrage; normalisation sans accents; ensembles en mémoire.
|
|
||||||
- Usage: léger boost de confiance si prénom/nom détectés appartiennent à l’annuaire.
|
|
||||||
- Extension multi-langues: ajouter des fichiers `firstnames_<lang>.csv` et `lastnames_<lang>.csv`.
|
|
||||||
|
|
||||||
Impact:
|
|
||||||
- Le score `globalConfidence` est augmenté de +5% pour prénom connu, +5% pour nom connu (max +10%).
|
|
||||||
- Améliore la décision de re-upload (moins de faux négatifs si noms valides).
|
|
||||||
@ -1,338 +0,0 @@
|
|||||||
# Architecture Backend pour le Traitement des Documents
|
|
||||||
|
|
||||||
## Vue d'ensemble
|
|
||||||
|
|
||||||
L'application utilise maintenant une architecture backend qui traite les données (OCR, NER) et renvoie du JSON au frontend. Cette approche améliore les performances et centralise le traitement des documents.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### 🏗️ Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
4NK_IA_front/
|
|
||||||
├── backend/ # Serveur backend Express
|
|
||||||
│ ├── server.js # Serveur principal
|
|
||||||
│ ├── package.json # Dépendances backend
|
|
||||||
│ └── uploads/ # Fichiers temporaires
|
|
||||||
├── src/ # Frontend React
|
|
||||||
│ ├── services/
|
|
||||||
│ │ ├── backendApi.ts # API backend
|
|
||||||
│ │ ├── openai.ts # Fallback local
|
|
||||||
│ │ └── ruleNer.ts # Règles NER
|
|
||||||
│ └── store/
|
|
||||||
│ └── documentSlice.ts # Redux avec backend
|
|
||||||
└── test-files/ # Fichiers de test
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔄 Flux de Données
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[Frontend React] --> B[Backend Express]
|
|
||||||
B --> C[Tesseract.js OCR]
|
|
||||||
B --> D[Règles NER]
|
|
||||||
C --> E[Texte extrait]
|
|
||||||
D --> F[Entités extraites]
|
|
||||||
E --> G[JSON Response]
|
|
||||||
F --> G
|
|
||||||
G --> A
|
|
||||||
```
|
|
||||||
|
|
||||||
## Backend (Express.js)
|
|
||||||
|
|
||||||
### 🚀 Serveur Principal
|
|
||||||
|
|
||||||
**Fichier**: `backend/server.js`
|
|
||||||
|
|
||||||
**Port**: 3001
|
|
||||||
|
|
||||||
**Endpoints**:
|
|
||||||
|
|
||||||
- `POST /api/extract` - Extraction de documents
|
|
||||||
- `GET /api/test-files` - Liste des fichiers de test
|
|
||||||
- `GET /api/health` - Health check
|
|
||||||
|
|
||||||
### 📄 Traitement des Documents
|
|
||||||
|
|
||||||
#### 1. Upload et Validation
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Configuration multer
|
|
||||||
const upload = multer({
|
|
||||||
storage: multer.diskStorage({...}),
|
|
||||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
|
|
||||||
fileFilter: (req, file, cb) => {
|
|
||||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/tiff', 'application/pdf']
|
|
||||||
// Validation des types de fichiers
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Extraction OCR Optimisée
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
async function extractTextFromImage(imagePath) {
|
|
||||||
const worker = await createWorker('fra+eng')
|
|
||||||
|
|
||||||
// Configuration optimisée pour cartes d'identité
|
|
||||||
const params = {
|
|
||||||
tessedit_pageseg_mode: '6',
|
|
||||||
tessedit_char_whitelist: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ...',
|
|
||||||
tessedit_ocr_engine_mode: '1', // LSTM
|
|
||||||
textord_min_xheight: '6', // Petits textes
|
|
||||||
// ... autres paramètres
|
|
||||||
}
|
|
||||||
|
|
||||||
await worker.setParameters(params)
|
|
||||||
const { data } = await worker.recognize(imagePath)
|
|
||||||
return { text: data.text, confidence: data.confidence }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Extraction NER par Règles
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function extractEntitiesFromText(text) {
|
|
||||||
const entities = {
|
|
||||||
identities: [],
|
|
||||||
addresses: [],
|
|
||||||
cniNumbers: [],
|
|
||||||
dates: [],
|
|
||||||
documentType: 'Document',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patterns pour cartes d'identité
|
|
||||||
const namePatterns = [
|
|
||||||
/(Vendeur|Acheteur|...)\s*:\s*([A-Z][a-zà-öø-ÿ'\-]+\s+[A-Z][a-zà-öø-ÿ'\-]+)/gi,
|
|
||||||
/^([A-Z][A-ZÀ-ÖØ-öø-ÿ\s\-']{2,30})$/gm,
|
|
||||||
// ... autres patterns
|
|
||||||
]
|
|
||||||
|
|
||||||
// Extraction des entités...
|
|
||||||
return entities
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📊 Réponse JSON
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"documentId": "doc-1234567890",
|
|
||||||
"fileName": "IMG_20250902_162159.jpg",
|
|
||||||
"fileSize": 1077961,
|
|
||||||
"mimeType": "image/jpeg",
|
|
||||||
"processing": {
|
|
||||||
"ocr": {
|
|
||||||
"text": "Texte extrait par OCR...",
|
|
||||||
"confidence": 85.5,
|
|
||||||
"wordCount": 25
|
|
||||||
},
|
|
||||||
"ner": {
|
|
||||||
"identities": [...],
|
|
||||||
"addresses": [...],
|
|
||||||
"cniNumbers": [...],
|
|
||||||
"dates": [...],
|
|
||||||
"documentType": "CNI"
|
|
||||||
},
|
|
||||||
"globalConfidence": 87.2
|
|
||||||
},
|
|
||||||
"extractedData": {
|
|
||||||
"documentType": "CNI",
|
|
||||||
"identities": [...],
|
|
||||||
"addresses": [...],
|
|
||||||
"cniNumbers": [...],
|
|
||||||
"dates": [...]
|
|
||||||
},
|
|
||||||
"timestamp": "2025-09-15T23:30:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Frontend (React)
|
|
||||||
|
|
||||||
### 🔌 Service Backend
|
|
||||||
|
|
||||||
**Fichier**: `src/services/backendApi.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function extractDocumentBackend(
|
|
||||||
documentId: string,
|
|
||||||
file?: File,
|
|
||||||
hooks?: {
|
|
||||||
onOcrProgress?: (progress: number) => void
|
|
||||||
onLlmProgress?: (progress: number) => void
|
|
||||||
},
|
|
||||||
): Promise<ExtractionResult> {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('document', file)
|
|
||||||
|
|
||||||
const response = await fetch(`${BACKEND_URL}/api/extract`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
|
|
||||||
const result: BackendExtractionResult = await response.json()
|
|
||||||
|
|
||||||
// Conversion vers le format frontend
|
|
||||||
return convertBackendToFrontend(result)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔄 Redux Store
|
|
||||||
|
|
||||||
**Fichier**: `src/store/documentSlice.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const extractDocument = createAsyncThunk(
|
|
||||||
'document/extract',
|
|
||||||
async (documentId: string, thunkAPI) => {
|
|
||||||
// Vérifier si le backend est disponible
|
|
||||||
const backendAvailable = await checkBackendHealth()
|
|
||||||
|
|
||||||
if (backendAvailable) {
|
|
||||||
// Utiliser le backend
|
|
||||||
return await backendDocumentApi.extract(documentId, file, progressHooks)
|
|
||||||
} else {
|
|
||||||
// Fallback vers le mode local
|
|
||||||
return await openaiDocumentApi.extract(documentId, file, progressHooks)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Démarrage
|
|
||||||
|
|
||||||
### 🚀 Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Option 1: Script automatique
|
|
||||||
./start-backend.sh
|
|
||||||
|
|
||||||
# Option 2: Manuel
|
|
||||||
cd backend
|
|
||||||
npm install
|
|
||||||
node server.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🌐 Frontend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🧪 Test de l'Architecture
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node test-backend-architecture.cjs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Avantages
|
|
||||||
|
|
||||||
### 🚀 Performance
|
|
||||||
|
|
||||||
- **Traitement centralisé** : OCR et NER sur le serveur
|
|
||||||
- **Optimisations** : Paramètres OCR optimisés pour les cartes d'identité
|
|
||||||
- **Cache** : Possibilité de mettre en cache les résultats
|
|
||||||
|
|
||||||
### 🔧 Maintenabilité
|
|
||||||
|
|
||||||
- **Séparation des responsabilités** : Backend pour le traitement, frontend pour l'UI
|
|
||||||
- **API REST** : Interface claire entre frontend et backend
|
|
||||||
- **Fallback** : Mode local en cas d'indisponibilité du backend
|
|
||||||
|
|
||||||
### 📊 Monitoring
|
|
||||||
|
|
||||||
- **Logs détaillés** : Traçabilité complète du traitement
|
|
||||||
- **Health check** : Vérification de l'état du backend
|
|
||||||
- **Métriques** : Confiance OCR, nombre d'entités extraites
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### 🔧 Variables d'Environnement
|
|
||||||
|
|
||||||
**Backend**:
|
|
||||||
|
|
||||||
- `PORT=3001` - Port du serveur backend
|
|
||||||
|
|
||||||
**Frontend**:
|
|
||||||
|
|
||||||
- `VITE_BACKEND_URL=http://localhost:3001` - URL du backend
|
|
||||||
- `VITE_USE_RULE_NER=true` - Mode règles locales (fallback)
|
|
||||||
- `VITE_DISABLE_LLM=true` - Désactiver LLM
|
|
||||||
|
|
||||||
### 📁 Structure des Fichiers
|
|
||||||
|
|
||||||
```
|
|
||||||
backend/
|
|
||||||
├── server.js # Serveur Express
|
|
||||||
├── package.json # Dépendances
|
|
||||||
└── uploads/ # Fichiers temporaires (auto-créé)
|
|
||||||
|
|
||||||
src/services/
|
|
||||||
├── backendApi.ts # API backend
|
|
||||||
├── openai.ts # Fallback local
|
|
||||||
└── ruleNer.ts # Règles NER
|
|
||||||
|
|
||||||
docs/
|
|
||||||
└── architecture-backend.md # Cette documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dépannage
|
|
||||||
|
|
||||||
### ❌ Problèmes Courants
|
|
||||||
|
|
||||||
#### Backend non accessible
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Vérifier que le backend est démarré
|
|
||||||
curl http://localhost:3001/api/health
|
|
||||||
|
|
||||||
# Vérifier les logs
|
|
||||||
cd backend && node server.js
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Erreurs OCR
|
|
||||||
|
|
||||||
- Vérifier la taille des images (minimum 3x3 pixels)
|
|
||||||
- Ajuster les paramètres `textord_min_xheight`
|
|
||||||
- Vérifier les types de fichiers supportés
|
|
||||||
|
|
||||||
#### Erreurs de communication
|
|
||||||
|
|
||||||
- Vérifier que les ports 3001 (backend) et 5176 (frontend) sont libres
|
|
||||||
- Vérifier la configuration CORS
|
|
||||||
- Vérifier les variables d'environnement
|
|
||||||
|
|
||||||
### 🔍 Logs
|
|
||||||
|
|
||||||
**Backend**:
|
|
||||||
|
|
||||||
```
|
|
||||||
🚀 Serveur backend démarré sur le port 3001
|
|
||||||
📡 API disponible sur: http://localhost:3001/api
|
|
||||||
[OCR] Début de l'extraction pour: uploads/document-123.jpg
|
|
||||||
[OCR] Extraction terminée - Confiance: 85.5%
|
|
||||||
[NER] Extraction terminée: 2 identités, 1 adresse, 1 CNI
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend**:
|
|
||||||
|
|
||||||
```
|
|
||||||
🚀 [STORE] Utilisation du backend pour l'extraction
|
|
||||||
📊 [PROGRESS] OCR doc-123: 30%
|
|
||||||
📊 [PROGRESS] NER doc-123: 50%
|
|
||||||
🎉 [BACKEND] Extraction terminée avec succès
|
|
||||||
```
|
|
||||||
|
|
||||||
## Évolutions Futures
|
|
||||||
|
|
||||||
### 🔮 Améliorations Possibles
|
|
||||||
|
|
||||||
1. **Base de données** : Stockage des résultats d'extraction
|
|
||||||
2. **Cache Redis** : Mise en cache des résultats OCR
|
|
||||||
3. **Queue système** : Traitement asynchrone des gros volumes
|
|
||||||
4. **API GraphQL** : Interface plus flexible
|
|
||||||
5. **Microservices** : Séparation OCR et NER
|
|
||||||
6. **Docker** : Containerisation pour le déploiement
|
|
||||||
7. **Monitoring** : Métriques et alertes
|
|
||||||
8. **Tests automatisés** : Suite de tests complète
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
# Changelog - Système de Pending
|
|
||||||
|
|
||||||
## Version 1.1.1 - 2025-09-16
|
|
||||||
|
|
||||||
### 🔧 Corrections critiques
|
|
||||||
|
|
||||||
- **Fix URL API** : Correction de l'URL de l'API de `http://localhost:18000` vers `http://localhost:3001/api`
|
|
||||||
- **Résolution des timeouts** : Le frontend peut maintenant contacter le backend correctement
|
|
||||||
- **Logs de debug** : Ajout de logs pour tracer les appels API et diagnostiquer les problèmes
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
### 🧼 Nettoyage Router – 2025-09-18
|
|
||||||
|
|
||||||
- Suppression de la dépendance redondante `router-dom` (conservée: `react-router-dom@^7.9.1`)
|
|
||||||
- Ré‑installation des modules (`npm ci`) pour régénérer le lockfile
|
|
||||||
- Impact: réduction du risque de conflits de dépendances et du poids inutile du bundle
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
title: Déduplication des entités extraites
|
|
||||||
description: Règles et clés de normalisation pour éliminer les doublons d'entités
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contexte
|
|
||||||
|
|
||||||
Afin d'éviter l'affichage et le traitement multiple de la même information, une déduplication déterministe a été ajoutée côté frontend après l'extraction des entités.
|
|
||||||
|
|
||||||
## Périmètre
|
|
||||||
|
|
||||||
- Identités (personnes)
|
|
||||||
- Adresses
|
|
||||||
- Dates
|
|
||||||
- Entreprises (nom, SIREN, SIRET)
|
|
||||||
- Signatures
|
|
||||||
- Références (type, valeur)
|
|
||||||
|
|
||||||
## Règles de déduplication
|
|
||||||
|
|
||||||
- Identités: clé = `lower(trim(firstName))|lower(trim(lastName))`
|
|
||||||
- Adresses: clé = `lower(trim(street))|trim(postalCode)|lower(trim(city))|lower(trim(country))`
|
|
||||||
- Dates: utilisation d'un `Set` (unicité par valeur exacte)
|
|
||||||
- Entreprises: clé = `lower(trim(name))|trim(siren)|trim(siret)`
|
|
||||||
- Signatures: utilisation d'un `Set` (unicité par valeur exacte)
|
|
||||||
- Références: clé = `lower(trim(type))|lower(trim(value))`
|
|
||||||
|
|
||||||
## Points d'intégration
|
|
||||||
|
|
||||||
- `src/services/ruleNer.ts`: déduplication appliquée au résultat du NER par règles.
|
|
||||||
- `src/services/backendApi.ts`: déduplication appliquée après le mapping de la réponse backend standard.
|
|
||||||
|
|
||||||
## Effets de bord
|
|
||||||
|
|
||||||
- L'ordre des entités est conservé (première occurrence préservée).
|
|
||||||
- Les entrées vides sont normalisées avant comparaison (trim/lowercase) pour limiter les faux doublons.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Un test ciblé `tests/deduplication.test.ts` vérifie que des doublons simples (noms/adresses répétés) sont éliminés.
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
{
|
|
||||||
"document": {
|
|
||||||
"id": "doc-1757976015681",
|
|
||||||
"fileName": "facture_4NK_08-2025_04.pdf",
|
|
||||||
"fileSize": 85819,
|
|
||||||
"mimeType": "application/pdf",
|
|
||||||
"uploadTimestamp": "2025-09-15T22:40:15.681Z"
|
|
||||||
},
|
|
||||||
"classification": {
|
|
||||||
"documentType": "Facture",
|
|
||||||
"confidence": 0.95,
|
|
||||||
"subType": "Facture de prestation",
|
|
||||||
"language": "fr",
|
|
||||||
"pageCount": 1
|
|
||||||
},
|
|
||||||
"extraction": {
|
|
||||||
"text": {
|
|
||||||
"raw": "Janin Consulting - EURL au capital de 500 Euros...",
|
|
||||||
"processed": "Janin Consulting - EURL au capital de 500 Euros...",
|
|
||||||
"wordCount": 165,
|
|
||||||
"characterCount": 1197,
|
|
||||||
"confidence": 0.95
|
|
||||||
},
|
|
||||||
"entities": {
|
|
||||||
"persons": [
|
|
||||||
{
|
|
||||||
"id": "person-1",
|
|
||||||
"type": "contact",
|
|
||||||
"firstName": "Anthony",
|
|
||||||
"lastName": "Janin",
|
|
||||||
"role": "Gérant",
|
|
||||||
"email": "ja.janin.anthony@gmail.com",
|
|
||||||
"phone": "33 (0)6 71 40 84 13",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"companies": [
|
|
||||||
{
|
|
||||||
"id": "company-1",
|
|
||||||
"name": "Janin Consulting",
|
|
||||||
"legalForm": "EURL",
|
|
||||||
"siret": "815 322 912 00040",
|
|
||||||
"rcs": "815 322 912 NANTERRE",
|
|
||||||
"tva": "FR64 815 322 912",
|
|
||||||
"capital": "500 Euros",
|
|
||||||
"role": "Fournisseur",
|
|
||||||
"confidence": 0.95,
|
|
||||||
"source": "rule-based"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "company-2",
|
|
||||||
"name": "4NK",
|
|
||||||
"tva": "FR79913422994",
|
|
||||||
"role": "Client",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"addresses": [
|
|
||||||
{
|
|
||||||
"id": "address-1",
|
|
||||||
"type": "siège_social",
|
|
||||||
"street": "177 rue du Faubourg Poissonnière",
|
|
||||||
"city": "Paris",
|
|
||||||
"postalCode": "75009",
|
|
||||||
"country": "France",
|
|
||||||
"company": "Janin Consulting",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "address-2",
|
|
||||||
"type": "facturation",
|
|
||||||
"street": "4 SQUARE DES GOELANDS",
|
|
||||||
"city": "MONT-SAINT-AIGNAN",
|
|
||||||
"postalCode": "76130",
|
|
||||||
"country": "France",
|
|
||||||
"company": "4NK",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"financial": {
|
|
||||||
"amounts": [
|
|
||||||
{
|
|
||||||
"id": "amount-1",
|
|
||||||
"type": "prestation",
|
|
||||||
"description": "Prestation du mois d'Août 2025",
|
|
||||||
"quantity": 10,
|
|
||||||
"unitPrice": 550.0,
|
|
||||||
"totalHT": 5500.0,
|
|
||||||
"currency": "EUR",
|
|
||||||
"confidence": 0.95
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"totals": {
|
|
||||||
"totalHT": 5500.0,
|
|
||||||
"totalTVA": 1100.0,
|
|
||||||
"totalTTC": 6600.0,
|
|
||||||
"tvaRate": 0.2,
|
|
||||||
"currency": "EUR"
|
|
||||||
},
|
|
||||||
"payment": {
|
|
||||||
"terms": "30 jours après émission",
|
|
||||||
"penaltyRate": "Taux BCE + 7 points",
|
|
||||||
"bankDetails": {
|
|
||||||
"bank": "CAISSE D'EPARGNE D'ILE DE FRANCE",
|
|
||||||
"accountHolder": "Janin Anthony",
|
|
||||||
"address": "1 rue Pasteur (78800)",
|
|
||||||
"rib": "17515006000800309088884"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dates": [
|
|
||||||
{
|
|
||||||
"id": "date-1",
|
|
||||||
"type": "facture",
|
|
||||||
"value": "29-août-25",
|
|
||||||
"formatted": "2025-08-29",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "date-2",
|
|
||||||
"type": "période",
|
|
||||||
"value": "août-25",
|
|
||||||
"formatted": "2025-08",
|
|
||||||
"confidence": 0.9,
|
|
||||||
"source": "rule-based"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"contractual": {
|
|
||||||
"clauses": [
|
|
||||||
{
|
|
||||||
"id": "clause-1",
|
|
||||||
"type": "paiement",
|
|
||||||
"content": "Le paiement se fera (maximum) 30 jours après l'émission de la facture.",
|
|
||||||
"confidence": 0.9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "clause-2",
|
|
||||||
"type": "intérêts_retard",
|
|
||||||
"content": "Tout retard de paiement d'une quelconque facture fait courir, immédiatement et de plein droit, des intérêts de retard calculés au taux directeur de la BCE majoré de 7 points jusqu'au paiement effectif et intégral.",
|
|
||||||
"confidence": 0.9
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"signatures": [
|
|
||||||
{
|
|
||||||
"id": "signature-1",
|
|
||||||
"type": "électronique",
|
|
||||||
"present": false,
|
|
||||||
"signatory": null,
|
|
||||||
"date": null,
|
|
||||||
"confidence": 0.8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"id": "ref-1",
|
|
||||||
"type": "facture",
|
|
||||||
"number": "4NK_4",
|
|
||||||
"confidence": 0.95
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"processing": {
|
|
||||||
"engine": "4NK_IA_Backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"processingTime": "2.5s",
|
|
||||||
"ocrEngine": "pdf-parse",
|
|
||||||
"nerEngine": "rule-based",
|
|
||||||
"preprocessing": {
|
|
||||||
"applied": false,
|
|
||||||
"reason": "PDF direct text extraction"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"quality": {
|
|
||||||
"globalConfidence": 0.95,
|
|
||||||
"textExtractionConfidence": 0.95,
|
|
||||||
"entityExtractionConfidence": 0.9,
|
|
||||||
"classificationConfidence": 0.95
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"success": true,
|
|
||||||
"errors": [],
|
|
||||||
"warnings": ["Aucune signature détectée"],
|
|
||||||
"timestamp": "2025-09-15T22:40:15.681Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
### Spécification UI – Onglet Extraction
|
|
||||||
|
|
||||||
Objectifs:
|
|
||||||
- Éditer/supprimer les entités extraites (personnes/adresses/entreprises)
|
|
||||||
- Lancer la collecte externe par entité, afficher le statut, proposer le PDF/JSON
|
|
||||||
- Déclencher automatiquement la collecte si l’entité n’a pas encore de données enrichies
|
|
||||||
|
|
||||||
Comportements:
|
|
||||||
- Bouton Collecter: passe en “Collecte…” (state=running) puis “OK” à la fin, avec lien “Voir PDF” et “Voir JSON”.
|
|
||||||
- Polling réduit: 1,5 s × 6 tentatives, puis “Relancer”.
|
|
||||||
- Boutons Enregistrer/Supprimer ne déclenchent pas de re-scan; ils patchent le cache uniquement.
|
|
||||||
|
|
||||||
Endpoints utilisés:
|
|
||||||
- POST /enrich/:kind → démarre la collecte
|
|
||||||
- GET /enrich/:kind/status → état et sources
|
|
||||||
- GET /enrich/:kind/pdf → PDF généré
|
|
||||||
|
|
||||||
Accessibilité:
|
|
||||||
- Actions groupées, labels explicites, tooltips d’aide, responsive.
|
|
||||||
|
|
||||||
### Onglets d’entités – 2025-09-18
|
|
||||||
|
|
||||||
- Onglets MUI pour naviguer entre les entités du document.
|
|
||||||
- Onglets retenus: Personnes, Adresses, Entreprises, Contractuel.
|
|
||||||
- Le badge de chaque onglet reflète le nombre d’éléments détectés.
|
|
||||||
- L’onglet initial est choisi automatiquement selon les données disponibles.
|
|
||||||
- L’édition/suppression et l’enrichissement restent disponibles dans les sections pertinentes.
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
### Adresses – Cadastre / GéoRisque / GéoFoncier
|
|
||||||
|
|
||||||
Objectif: géocoder l’adresse, identifier les parcelles cadastrales, lister les risques majeurs, et produire un PDF de synthèse avec liens sources.
|
|
||||||
|
|
||||||
Entrées minimales:
|
|
||||||
- street, postalCode, city, country ('France')
|
|
||||||
|
|
||||||
Accès:
|
|
||||||
- Géocodage: BAN/Adresse DataGouv (open data)
|
|
||||||
- GéoRisque: couches publiques par bbox
|
|
||||||
- Cadastre: plan cadastral open data (WMS/WMTS)
|
|
||||||
- GéoFoncier: page publique par lat/lon (scraping léger si autorisé)
|
|
||||||
|
|
||||||
Sortie (cache JSON):
|
|
||||||
```
|
|
||||||
extraction.entities.addresses[i].enrichment.address = {
|
|
||||||
state,
|
|
||||||
startedAt, finishedAt,
|
|
||||||
geocode: { lat, lon, score },
|
|
||||||
cadastre: [{ section, parcelle, commune }],
|
|
||||||
risks: [{ type, level, sourceUrl }],
|
|
||||||
pdfPath
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
PDF: synthèse (adresse normalisée, mini-carte, risques) + parcelles.
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
### Bodacc – Gel des avoirs (Personnes)
|
|
||||||
|
|
||||||
Objectif: vérifier l’existence de mentions de gel des avoirs pour une identité (NOM/PRÉNOM), consigner les hits (références, lien source) et produire un PDF de synthèse.
|
|
||||||
|
|
||||||
Entrées minimales:
|
|
||||||
- lastName (obligatoire, uppercase recommandé)
|
|
||||||
- firstName (optionnel)
|
|
||||||
- métadonnées utiles: date de naissance, pays
|
|
||||||
|
|
||||||
Accès:
|
|
||||||
- Sans clé (temporaire): scraping léger avec politesse (User-Agent dédié, 1 req/s, backoff).
|
|
||||||
- Avec API (si disponible): clé API, endpoint de recherche nom/prénom.
|
|
||||||
|
|
||||||
Sortie (dans le cache JSON):
|
|
||||||
```
|
|
||||||
extraction.entities.persons[i].enrichment.bodacc = {
|
|
||||||
state: 'idle'|'running'|'done'|'error',
|
|
||||||
startedAt, finishedAt,
|
|
||||||
hits: [{ name, date, sourceUrl, matchScore }],
|
|
||||||
pdfPath
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
PDF: une page de synthèse (identité + hits) + annexes si autorisé.
|
|
||||||
|
|
||||||
Limites: homonymies; respecter le cadre légal (RGPD) et citer les sources.
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
### Inforgreffe – Extrait Kbis (Entreprises) (+ Societe.com)
|
|
||||||
|
|
||||||
Objectif: récupérer une fiche société (forme, siren, dirigeants, adresse) et idéalement un extrait Kbis.
|
|
||||||
|
|
||||||
Entrées minimales:
|
|
||||||
- name (raison sociale) OU siren/siret
|
|
||||||
|
|
||||||
Accès:
|
|
||||||
- Sans clé (temporaire): recherche Societe.com (résumé public) et page Inforgreffe (si visible) en scraping léger (1 req/s, backoff).
|
|
||||||
- Avec API: clé/secret; endpoints “Profil entreprise”, “Kbis PDF”.
|
|
||||||
|
|
||||||
Sortie (cache JSON):
|
|
||||||
```
|
|
||||||
extraction.entities.companies[i].enrichment.company = {
|
|
||||||
state,
|
|
||||||
startedAt, finishedAt,
|
|
||||||
sources: ['societe_com','inforgreffe'],
|
|
||||||
profile: { siren, siret, forme, capital, adresse, dirigeants: [] },
|
|
||||||
pdfPath
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
PDF: synthèse (identité, dirigeants) + annexes Kbis si autorisé, sinon résumé horodaté + liens sources.
|
|
||||||
|
|
||||||
Conformité: respecter CGU, citer les sources, logs de collecte.
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
# Redesign Interface Extraction - 18/09/2025
|
|
||||||
|
|
||||||
## Modifications apportées
|
|
||||||
|
|
||||||
### 1. Redesign complet de l'interface Extraction
|
|
||||||
- **Remplacement de Grid par Box/Stack** : Suppression des composants Grid Material-UI problématiques
|
|
||||||
- **Layout moderne** : Utilisation de Box avec flexbox pour un design responsive
|
|
||||||
- **Composants Material-UI** : Cards, Avatars, Badges, Chips pour une interface professionnelle
|
|
||||||
- **Navigation sidebar** : Liste des documents avec sélection visuelle
|
|
||||||
- **Métadonnées techniques** : Accordion pour les informations de traitement
|
|
||||||
|
|
||||||
### 2. Repositionnement du texte extrait
|
|
||||||
- **Déplacement en bas** : Section "Texte extrait" maintenant en fin de page
|
|
||||||
- **Toggle d'affichage** : Bouton pour masquer/afficher le contenu
|
|
||||||
- **Style monospace** : Police monospace pour une meilleure lisibilité
|
|
||||||
|
|
||||||
### 3. Affichage des résultats de collecte
|
|
||||||
- **Boutons d'enrichissement intelligents** :
|
|
||||||
- **Personnes** : "Bodacc ✓" quand collecte terminée
|
|
||||||
- **Adresses** : "BAN+GéoRisque+Cadastre ✓" quand collecte terminée
|
|
||||||
- **Entreprises** : "Inforgreffe+Societe.com ✓" quand collecte terminée
|
|
||||||
- **États visuels** : Collecte en cours, terminée, erreur
|
|
||||||
- **Bases collectées** : Affichage explicite des sources de données
|
|
||||||
|
|
||||||
### 4. Configuration et déploiement
|
|
||||||
- **Nginx proxy** : Configuration du proxy vers backend port 3001
|
|
||||||
- **Build Vite** : Compilation et déploiement vers `/usr/share/nginx/html`
|
|
||||||
- **Services relancés** : PM2 backend + Nginx redémarrés
|
|
||||||
- **Connectivité vérifiée** : API accessible via HTTPS
|
|
||||||
|
|
||||||
## Structure de l'interface
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Header avec actions (Re-traiter) │
|
|
||||||
├─────────────────────────────────────────────────────────┤
|
|
||||||
│ ┌─────────────┐ ┌─────────────────────────────────────┐ │
|
|
||||||
│ │ Sidebar │ │ Contenu principal │ │
|
|
||||||
│ │ Documents │ │ ┌─────────────────────────────────┐ │ │
|
|
||||||
│ │ - Doc 1 │ │ │ Métadonnées du document │ │ │
|
|
||||||
│ │ - Doc 2 │ │ └─────────────────────────────────┘ │ │
|
|
||||||
│ │ - Doc 3 │ │ ┌─────────────────────────────────┐ │ │
|
|
||||||
│ └─────────────┘ │ │ Entités extraites │ │ │
|
|
||||||
│ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │
|
|
||||||
│ │ │ │Pers.│ │Addr.│ │Entr.│ │ │ │
|
|
||||||
│ │ │ └─────┘ └─────┘ └─────┘ │ │ │
|
|
||||||
│ │ └─────────────────────────────────┘ │ │
|
|
||||||
│ │ ┌─────────────────────────────────┐ │ │
|
|
||||||
│ │ │ Métadonnées techniques │ │ │
|
|
||||||
│ │ └─────────────────────────────────┘ │ │
|
|
||||||
│ │ ┌─────────────────────────────────┐ │ │
|
|
||||||
│ │ │ Texte extrait (toggle) │ │ │
|
|
||||||
│ │ └─────────────────────────────────┘ │ │
|
|
||||||
│ └─────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Bases de données collectées
|
|
||||||
|
|
||||||
### Personnes
|
|
||||||
- **Bodacc** : Gel des avoirs, sanctions financières
|
|
||||||
- **Statut** : Affiché sur le bouton d'enrichissement
|
|
||||||
|
|
||||||
### Adresses
|
|
||||||
- **BAN** : Base Adresse Nationale (géocodage)
|
|
||||||
- **GéoRisque** : Risques majeurs (inondations, séismes, etc.)
|
|
||||||
- **Cadastre** : Parcelles cadastrales
|
|
||||||
- **Statut** : Affiché sur le bouton d'enrichissement
|
|
||||||
|
|
||||||
### Entreprises
|
|
||||||
- **Inforgreffe** : Extrait Kbis, informations légales
|
|
||||||
- **Societe.com** : Fiche entreprise, dirigeants
|
|
||||||
- **Statut** : Affiché sur le bouton d'enrichissement
|
|
||||||
|
|
||||||
## Tests de fonctionnement
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
```bash
|
|
||||||
curl -s https://ia.4nkweb.com/api/health
|
|
||||||
# {"status":"OK","timestamp":"2025-09-18T17:07:06.312Z","version":"1.0.0","metrics":{"pending":0,"results":5}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- Interface accessible via HTTPS
|
|
||||||
- Navigation entre documents fonctionnelle
|
|
||||||
- Boutons d'enrichissement avec statuts
|
|
||||||
- Texte extrait en bas de page
|
|
||||||
|
|
||||||
## Prochaines étapes
|
|
||||||
|
|
||||||
1. **Tests utilisateur** : Validation de l'ergonomie
|
|
||||||
2. **Optimisations** : Performance et responsive design
|
|
||||||
3. **Fonctionnalités** : Ajout de nouvelles sources de données
|
|
||||||
4. **Documentation** : Guides utilisateur et API
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
## Améliorations OCR CNI et Adresses
|
|
||||||
|
|
||||||
### CNI (Carte Nationale d'Identité)
|
|
||||||
|
|
||||||
- MRZ: extraction stricte avec motif `NOM<<PRENOMS` en majuscules et sans accents.
|
|
||||||
- Heuristique: si MRZ absente, repli `IDFRA` + prénom détecté avant `<<`.
|
|
||||||
- Libellés FR: prise en charge de `NOM:` et `PRÉNOM:` (ou `PRENOM:`), normalisation des accents.
|
|
||||||
- Déduplication/priorisation: MRZ prioritaire; nettoyage et validation des noms.
|
|
||||||
|
|
||||||
Fichiers impactés: `backend/server.js` (fonction `extractEntitiesFromText`).
|
|
||||||
|
|
||||||
### Adresses (France)
|
|
||||||
|
|
||||||
- Regex renforcée: `NUMERO + VOIE, CP(\d{5}) + VILLE (+ France optionnel)`.
|
|
||||||
- Variantes: libellés `Adresse:`, `Siège:`, `Adresse de facturation:` ou `demeurant ...`.
|
|
||||||
- Normalisations: suppression du suffixe `France` dans `city`, CP forcé à 5 chiffres.
|
|
||||||
|
|
||||||
Fichiers impactés: `backend/server.js` (motif `addressPatterns`).
|
|
||||||
|
|
||||||
### Tests rapides (manuels)
|
|
||||||
|
|
||||||
1) Charger un dossier et téléverser une CNI.
|
|
||||||
2) Vérifier `/api/folders/<hash>/results` et constater dans `entities.identities` un objet avec `lastName` et `firstName` extraits du document.
|
|
||||||
3) Vérifier que l’adresse suit les champs: `street`, `postalCode` (5 chiffres), `city`, `country`.
|
|
||||||
|
|
||||||
### Journal
|
|
||||||
|
|
||||||
- 2025-09-18: ajout MRZ stricte, heuristique `IDFRA`, libellés FR; regex adresse FR; normalisations.
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
# Optimisations UX - Performance et Accessibilité
|
|
||||||
|
|
||||||
## 🚀 Optimisations de Performance
|
|
||||||
|
|
||||||
### 1. Hooks de Performance (`usePerformance.ts`)
|
|
||||||
- **Mesure du temps de rendu** : Suivi des performances de rendu des composants
|
|
||||||
- **Utilisation mémoire** : Surveillance de l'utilisation de la mémoire JavaScript
|
|
||||||
- **Latence réseau** : Mesure des temps de réponse des API
|
|
||||||
- **Taux de cache** : Suivi de l'efficacité du cache
|
|
||||||
|
|
||||||
### 2. Composants Optimisés
|
|
||||||
|
|
||||||
#### LazyImage (`LazyImage.tsx`)
|
|
||||||
- **Chargement paresseux** : Images chargées uniquement quand elles entrent dans le viewport
|
|
||||||
- **Intersection Observer** : Détection efficace de la visibilité
|
|
||||||
- **Placeholders** : Squelettes de chargement pour une meilleure UX
|
|
||||||
- **Gestion d'erreurs** : Affichage d'un message en cas d'échec de chargement
|
|
||||||
|
|
||||||
#### VirtualizedList (`VirtualizedList.tsx`)
|
|
||||||
- **Rendu virtuel** : Affichage uniquement des éléments visibles
|
|
||||||
- **Chargement infini** : Support du scroll infini avec observer
|
|
||||||
- **Optimisation mémoire** : Réduction de l'utilisation mémoire pour de grandes listes
|
|
||||||
- **Performance** : Rendu fluide même avec des milliers d'éléments
|
|
||||||
|
|
||||||
### 3. Optimisations du Backend
|
|
||||||
- **Collecteurs asynchrones** : RBE, GéoFoncier, Bodacc, Inforgreffe
|
|
||||||
- **Cache intelligent** : Mise en cache des résultats d'extraction
|
|
||||||
- **Compression** : Réduction de la taille des réponses
|
|
||||||
- **Timeouts** : Gestion des requêtes longues
|
|
||||||
|
|
||||||
## ♿ Améliorations d'Accessibilité
|
|
||||||
|
|
||||||
### 1. Hook d'Accessibilité (`useAccessibility.ts`)
|
|
||||||
- **Navigation clavier** : Détection automatique de la navigation au clavier
|
|
||||||
- **Préférences système** : Respect des préférences de contraste et de mouvement
|
|
||||||
- **Annonces** : Support des lecteurs d'écran
|
|
||||||
- **Focus visible** : Gestion du focus pour la navigation clavier
|
|
||||||
|
|
||||||
### 2. Styles d'Accessibilité (`accessibility.css`)
|
|
||||||
- **Contraste élevé** : Support du mode contraste élevé
|
|
||||||
- **Réduction de mouvement** : Respect des préférences de mouvement
|
|
||||||
- **Focus visible** : Indicateurs de focus clairs
|
|
||||||
- **Tailles minimales** : Éléments interactifs de 44px minimum
|
|
||||||
|
|
||||||
### 3. Fonctionnalités d'Accessibilité
|
|
||||||
- **ARIA** : Attributs ARIA pour les lecteurs d'écran
|
|
||||||
- **Navigation clavier** : Support complet de la navigation au clavier
|
|
||||||
- **Annonces** : Notifications pour les changements d'état
|
|
||||||
- **Sémantique** : Structure HTML sémantique
|
|
||||||
|
|
||||||
## 📊 Métriques de Performance
|
|
||||||
|
|
||||||
### Métriques Surveillées
|
|
||||||
- **Temps de rendu** : < 16ms pour 60 FPS
|
|
||||||
- **Utilisation mémoire** : < 80% de la limite
|
|
||||||
- **Latence réseau** : < 200ms pour les API
|
|
||||||
- **Taux de cache** : > 80% pour les requêtes répétées
|
|
||||||
|
|
||||||
### Outils de Mesure
|
|
||||||
- **Performance API** : Mesure native des performances
|
|
||||||
- **Intersection Observer** : Détection de visibilité
|
|
||||||
- **ResizeObserver** : Surveillance des changements de taille
|
|
||||||
- **Custom hooks** : Hooks personnalisés pour les métriques
|
|
||||||
|
|
||||||
## 🎯 Bonnes Pratiques
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
1. **Lazy loading** : Chargement paresseux des composants et images
|
|
||||||
2. **Memoization** : Utilisation de `useMemo` et `useCallback`
|
|
||||||
3. **Virtualisation** : Rendu virtuel pour les grandes listes
|
|
||||||
4. **Cache** : Mise en cache des données et composants
|
|
||||||
5. **Compression** : Optimisation des assets et réponses
|
|
||||||
|
|
||||||
### Accessibilité
|
|
||||||
1. **Navigation clavier** : Support complet de la navigation au clavier
|
|
||||||
2. **Lecteurs d'écran** : Attributs ARIA et annonces
|
|
||||||
3. **Contraste** : Respect des standards de contraste
|
|
||||||
4. **Focus** : Gestion claire du focus
|
|
||||||
5. **Sémantique** : Structure HTML sémantique
|
|
||||||
|
|
||||||
## 🔧 Configuration
|
|
||||||
|
|
||||||
### Variables d'Environnement
|
|
||||||
```bash
|
|
||||||
# Performance
|
|
||||||
VITE_PERFORMANCE_MONITORING=true
|
|
||||||
VITE_CACHE_TTL=300000
|
|
||||||
|
|
||||||
# Accessibilité
|
|
||||||
VITE_ACCESSIBILITY_ANNOUNCEMENTS=true
|
|
||||||
VITE_HIGH_CONTRAST_SUPPORT=true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration des Hooks
|
|
||||||
```typescript
|
|
||||||
// Performance
|
|
||||||
const { metrics, startRenderTimer, endRenderTimer } = usePerformance()
|
|
||||||
|
|
||||||
// Accessibilité
|
|
||||||
const { state, announceToScreenReader } = useAccessibility()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 Monitoring
|
|
||||||
|
|
||||||
### Métriques en Temps Réel
|
|
||||||
- **Console** : Logs de performance dans la console
|
|
||||||
- **Métriques** : Affichage des métriques dans l'interface
|
|
||||||
- **Alertes** : Notifications en cas de problème de performance
|
|
||||||
|
|
||||||
### Tests d'Accessibilité
|
|
||||||
- **Navigation clavier** : Test de la navigation au clavier
|
|
||||||
- **Lecteurs d'écran** : Test avec NVDA/JAWS
|
|
||||||
- **Contraste** : Vérification des contrastes
|
|
||||||
- **Focus** : Test de la gestion du focus
|
|
||||||
|
|
||||||
## 🚀 Déploiement
|
|
||||||
|
|
||||||
### Optimisations de Production
|
|
||||||
- **Minification** : Code minifié et optimisé
|
|
||||||
- **Compression** : Assets compressés (gzip/brotli)
|
|
||||||
- **Cache** : Headers de cache appropriés
|
|
||||||
- **CDN** : Utilisation d'un CDN pour les assets
|
|
||||||
|
|
||||||
### Monitoring de Production
|
|
||||||
- **Métriques** : Surveillance des métriques en production
|
|
||||||
- **Erreurs** : Gestion des erreurs et logging
|
|
||||||
- **Performance** : Monitoring des performances
|
|
||||||
- **Accessibilité** : Tests d'accessibilité automatisés
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
## PM2
|
|
||||||
|
|
||||||
- watch backend (backend/*), ignore uploads/cache
|
|
||||||
- restart on changes, logs dans log/
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
## Gestion de la qualité: remplacement d'image, confirmation d'adresse et révision IA
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Suggestions ajoutées dans `status.suggestions` des résultats:
|
|
||||||
- `needsReupload` (bool), `reasons` (array)
|
|
||||||
- `needsAddressConfirmation` (bool), `detectedAddress` (objet)
|
|
||||||
- Endpoint confirmation d'adresse:
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- Annuaire de noms (FR/EN) intégré: rehausse la confiance si prénom/nom reconnus dans les listes unifiées.
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
### Révision IA (Ollama): scoring, corrections et avis
|
|
||||||
|
|
||||||
#### Objectif
|
|
||||||
Fournir une évaluation automatique de la fiabilité des extractions (score), proposer des corrections potentielles (champ, valeur, confiance) et un avis court, en s’appuyant sur un LLM local accessible via Ollama.
|
|
||||||
|
|
||||||
#### Composants et endpoints
|
|
||||||
- Backend:
|
|
||||||
- Appel automatique après chaque extraction: intégration du résultat de révision dans `status.review` et `metadata.quality.ollamaScore`.
|
|
||||||
- Endpoint manuel: `POST /api/folders/:folderHash/files/:fileHash/review` (pas de payload requis). Retourne `{ success, review }`.
|
|
||||||
- Format attendu du LLM (réponse stricte JSON): `{ "score": number (0..1), "corrections": Array<{ path, value, confidence }>, "avis": string }`.
|
|
||||||
- Frontend:
|
|
||||||
- Bouton “Révision IA” par document (état completed): déclenche l’endpoint manuel puis rafraîchit la liste.
|
|
||||||
- Affichage: Chip `IA: x.xx` (tooltip = `avis` si présent). Chip `Corrections: N` ouvrant un dialogue listant les corrections.
|
|
||||||
|
|
||||||
#### Calcul des scores et arbitrage
|
|
||||||
- Base OCR: confiance Tesseract/pdf-parse normalisée.
|
|
||||||
- Boost annuaire de noms (`backend/data/names/firstnames_all.csv`, `lastnames_all.csv`): +0.05 si prénom trouvé, +0.05 si nom trouvé (après normalisation), agrégé au score.
|
|
||||||
- Ensembles de règles NER (CNI/MRZ, adresses, entités) influent indirectement via `identities` et `cniNumbers` (poids renforcés).
|
|
||||||
- Score global côté backend (avant Ollama): plafonné à 0.99, agrège OCR, présence d’identités, CNI, boost annuaire.
|
|
||||||
- Révision Ollama: si `review.score` est supérieur au `globalConfidence`, le backend rehausse `metadata.quality.globalConfidence` à ce score et persiste le résultat.
|
|
||||||
|
|
||||||
#### Suggestions de qualité
|
|
||||||
- `needsReupload`: déclenché si confiance OCR < 0.75, ou si CNI sans NOM/PRÉNOM.
|
|
||||||
- `needsAddressConfirmation`: déclenché si une adresse est détectée; confirmation côté UI via dialogue et endpoint `confirm-address`.
|
|
||||||
|
|
||||||
#### Dépendances et configuration
|
|
||||||
- Ollama: service HTTP local sur `http://localhost:11434`. Modèle configurable via `OLLAMA_MODEL` (défaut `llama3.1`). Timeout 8s.
|
|
||||||
- Aucune dépendance Node additionnelle (utilisation d’`http` natif).
|
|
||||||
|
|
||||||
#### Données persistées
|
|
||||||
- Cache fichier par document: `cache/<folderHash>/<fileHash>.json`.
|
|
||||||
- Champs ajoutés/modifiés:
|
|
||||||
- `metadata.quality.ollamaScore: number` (0..1)
|
|
||||||
- `status.review: { score, corrections[], avis }`
|
|
||||||
- `metadata.quality.globalConfidence: number` (peut être rehaussé par Ollama)
|
|
||||||
|
|
||||||
#### UI et interactions
|
|
||||||
- Liste des documents:
|
|
||||||
- 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.
|
|
||||||
- Cliquer “Révision IA”: confirmer que la liste se rafraîchit et que `status.review` est renseigné.
|
|
||||||
- Ouvrir le dialogue des corrections et vérifier l’affichage des champs.
|
|
||||||
- Confirmer/infirmer une adresse détectée et vérifier la mise à jour côté backend et disparition du flag.
|
|
||||||
|
|
||||||
#### Journal des décisions
|
|
||||||
- Choix du JSON strict pour la sortie LLM afin de faciliter l’exploitation et éviter les parsings fragiles.
|
|
||||||
- Utilisation d’`http` natif côté Node pour éviter l’ajout de dépendances.
|
|
||||||
@ -1,186 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
## Objet
|
|
||||||
|
|
||||||
Décrire le pipeline de traitement des images et comment diagnostiquer un blocage.
|
|
||||||
|
|
||||||
### Contexte
|
|
||||||
|
|
||||||
- Backend: Node.js/Express, OCR: tesseract.js, prétraitement: sharp, PDF: pdf-parse.
|
|
||||||
- Répertoires: `uploads/<folderHash>/` (entrées) et `cache/<folderHash>/` (résultats JSON).
|
|
||||||
- Métadonnées ignorées: `folder.json`, `*.meta`.
|
|
||||||
|
|
||||||
### Pipeline
|
|
||||||
|
|
||||||
1. Découverte des fichiers (ignore métadonnées)
|
|
||||||
2. Prétraitement image (grayscale, normalisation, contraste, débruitage)
|
|
||||||
3. OCR (multi-pass `ocrb+eng`, fallback `eng`)
|
|
||||||
4. Extraction PDF (pdf-parse ou OCR si scanné)
|
|
||||||
5. NER/Classification (règles personnes/entreprises/adresses, type doc)
|
|
||||||
6. Écriture JSON dans `cache/<hash>/<fileHash>.json`
|
|
||||||
|
|
||||||
### Délais attendus
|
|
||||||
|
|
||||||
- JPEG ~1 Mo: 45–120 s (par image)
|
|
||||||
- PDF texte: 0.2–2 s; PDF scanné: 30–90 s/page
|
|
||||||
- 2 images: 3–6 min au total (normal)
|
|
||||||
|
|
||||||
### Vérifications rapides
|
|
||||||
|
|
||||||
- Santé backend: `GET /api/health` → `{ status: "OK" }`
|
|
||||||
- État dossier: `GET /api/folders/<hash>/results`
|
|
||||||
- `hasPending` true si traitements restants
|
|
||||||
- `pending[].timestamp` récent
|
|
||||||
- `results[].document.fileName` présent
|
|
||||||
- Fichiers cache: apparition de `cache/<hash>/<fileHash>.json`
|
|
||||||
|
|
||||||
### Signes de blocage
|
|
||||||
|
|
||||||
- `hasPending: true` > 10 min sans nouveaux JSON dans `cache/<hash>/`
|
|
||||||
- Logs erreurs répétées (ex: type de fichier non supporté)
|
|
||||||
- Port 3001 occupé (EADDRINUSE)
|
|
||||||
|
|
||||||
### Actions correctives
|
|
||||||
|
|
||||||
- Redémarrage simple:
|
|
||||||
- `pkill -9 -f 'node.*backend/server.js' || true`
|
|
||||||
- `nohup node backend/server.js > backend.log 2>&1 &`
|
|
||||||
- Vérifier l’ignorance des métadonnées dans `backend/server.js`
|
|
||||||
- `if (file === 'folder.json' || file.endsWith('.meta')) { continue }`
|
|
||||||
- Recalcul pending: relancer l’endpoint results puis vérifier `pending`
|
|
||||||
|
|
||||||
### Qualité OCR CNI (note)
|
|
||||||
|
|
||||||
- Multi-pass Tesseract, amélioration d’image, regex adresse renforcée, MRZ si présent
|
|
||||||
- Cas cible: détecter les noms et prénoms selon qualité du scan
|
|
||||||
|
|
||||||
### État courant (vérifié)
|
|
||||||
|
|
||||||
- Backend UP; dossier `7d99a85daf66a0081a0e881630e6b39b`
|
|
||||||
- `results`: 3 fichiers traités; `pending`: 0 (suite correctif normalisation)
|
|
||||||
|
|
||||||
### Correctif appliqué (normalisation Sharp)
|
|
||||||
|
|
||||||
- Problème: erreurs OCR « Expected number between 1 and 100 for upper but received 0.9 » dues à `normalize({ lower: 0.1, upper: 0.9 })`.
|
|
||||||
- Cause: Sharp attend des percentiles entiers 1..100 pour `lower`/`upper`.
|
|
||||||
- Correctif: `backend/enhancedOcr.js` → `normalize({ lower: 10, upper: 90 })`.
|
|
||||||
- Effet: fin des boucles d’erreurs; `hasPending: false`.
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "doc_20250910_232208_10",
|
|
||||||
"filename": "facture_4NK_08-2025_04.pdf",
|
|
||||||
"size": 85819,
|
|
||||||
"upload_time": "2025-09-10T23:22:08.239575",
|
|
||||||
"status": "completed",
|
|
||||||
"progress": 100,
|
|
||||||
"current_step": "Terminé",
|
|
||||||
"results": {
|
|
||||||
"ocr_text": "Texte extrait simulé du document...",
|
|
||||||
"document_type": "Acte de vente",
|
|
||||||
"entities": {
|
|
||||||
"persons": ["Jean Dupont", "Marie Martin"],
|
|
||||||
"addresses": ["123 Rue de la Paix, 75001 Paris"],
|
|
||||||
"properties": ["Appartement T3, 75m²"]
|
|
||||||
},
|
|
||||||
"verification_score": 0.85,
|
|
||||||
"external_checks": { "cadastre": "OK", "georisques": "OK", "bodacc": "OK" }
|
|
||||||
},
|
|
||||||
"completion_time": "2025-09-10T23:22:18.243146"
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
apps: [
|
|
||||||
{
|
|
||||||
name: '4nk-ia-backend',
|
|
||||||
script: 'backend/server.js',
|
|
||||||
cwd: __dirname,
|
|
||||||
env: {
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
PORT: 3001,
|
|
||||||
OLLAMA_MIN_REVIEW_MS: process.env.OLLAMA_MIN_REVIEW_MS || 0,
|
|
||||||
},
|
|
||||||
instances: 1,
|
|
||||||
exec_mode: 'fork',
|
|
||||||
autorestart: true,
|
|
||||||
max_memory_restart: '512M',
|
|
||||||
out_file: './log/backend.out.log',
|
|
||||||
error_file: './log/backend.err.log',
|
|
||||||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
|
||||||
time: true,
|
|
||||||
watch: ['backend'],
|
|
||||||
ignore_watch: ['uploads', 'cache', 'node_modules', 'log'],
|
|
||||||
watch_delay: 1000,
|
|
||||||
exp_backoff_restart_delay: 200,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
BIN
eng.traineddata
BIN
eng.traineddata
Binary file not shown.
@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'
|
|||||||
import { globalIgnores } from 'eslint/config'
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
export default tseslint.config([
|
export default tseslint.config([
|
||||||
globalIgnores(['dist', 'coverage', 'backend/node_modules', 'node_modules']),
|
globalIgnores(['dist']),
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
extends: [
|
extends: [
|
||||||
@ -19,14 +19,5 @@ export default tseslint.config([
|
|||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
// Autoriser variables prefixées par _ pour marquer l'inutilisation intentionnelle
|
|
||||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrors: 'all', caughtErrorsIgnorePattern: '^_' }],
|
|
||||||
// Éviter les blocs vides involontaires mais tolérer try/catch vides si commentés
|
|
||||||
'no-empty': ['warn', { allowEmptyCatch: true }],
|
|
||||||
// Tolérer les échappements superflus dans certains regex hérités
|
|
||||||
'no-useless-escape': 'off',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
BIN
fra.traineddata
BIN
fra.traineddata
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
340329
|
|
||||||
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>IA - Lecoffre.io</title>
|
<title>Vite + React + TS</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
33
nginx.conf
33
nginx.conf
@ -1,33 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
root /var/www/ia.4nkweb.com/current;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Proxy vers le backend API
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://localhost:3001;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_connect_timeout 75s;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cache des assets
|
|
||||||
location /assets/ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
# SPA fallback
|
|
||||||
location / {
|
|
||||||
try_files $uri /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3225
package-lock.json
generated
3225
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@ -1,36 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "4nk-ia-front4nk",
|
"name": "4nk-ia-front4nk",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.4",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "node scripts/check-node.mjs",
|
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"prebuild": "node scripts/check-node.mjs",
|
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"clean:all": "rm -rf node_modules backend/node_modules dist cache log uploads backend/cache backend/uploads && mkdir -p uploads cache log backend/uploads backend/cache && npm ci --no-audit --no-fund && (cd backend && npm ci --no-audit --no-fund)",
|
|
||||||
"back:up": "npx --yes pm2 start ecosystem.config.cjs --only 4nk-ia-backend --env production",
|
|
||||||
"back:restart": "npx --yes pm2 restart 4nk-ia-backend || npx --yes pm2 start ecosystem.config.cjs --only 4nk-ia-backend --env production",
|
|
||||||
"prefront:up": "bash -lc 'p=$(ss -ltnp | grep :5174 | sed -n \"s/.*pid=\\([0-9]*\\).*/\\1/p\"); if [ -n \"$p\" ]; then kill $p; fi'",
|
|
||||||
"front:up": "vite",
|
|
||||||
"start:all": "npm run clean:all && concurrently -k -n back,front -c blue,green \"npm:back:restart\" \"npm:front:up\"",
|
|
||||||
"start:dev": "npm run clean:all && npm run back:restart && npm run front:up",
|
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --check .",
|
||||||
"format:fix": "prettier --write .",
|
"format:fix": "prettier --write .",
|
||||||
"mdlint": "markdownlint . --ignore node_modules --ignore dist",
|
"mdlint": "markdownlint . --ignore node_modules --ignore dist",
|
||||||
"test": "vitest run --coverage",
|
"test": "vitest run --coverage",
|
||||||
"test:ui": "vitest",
|
"test:ui": "vitest"
|
||||||
"test:collectors": "vitest run tests/collectors-simple.test.js",
|
|
||||||
"test:ocr": "vitest run tests/ocr.test.js",
|
|
||||||
"test:api": "vitest run tests/api.test.js",
|
|
||||||
"test:e2e": "vitest run tests/e2e.test.js",
|
|
||||||
"test:all": "npm run test:collectors && npm run test:ocr && npm run test:api && npm run test:e2e"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.19.0 <23",
|
|
||||||
"npm": ">=10"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
@ -39,17 +21,11 @@
|
|||||||
"@mui/material": "^7.3.2",
|
"@mui/material": "^7.3.2",
|
||||||
"@reduxjs/toolkit": "^2.9.0",
|
"@reduxjs/toolkit": "^2.9.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"canvas": "^3.2.0",
|
|
||||||
"jimp": "^1.6.0",
|
|
||||||
"pdf-parse": "^1.1.1",
|
|
||||||
"pdf-poppler": "^0.2.1",
|
|
||||||
"pdf2pic": "^3.2.0",
|
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.9.1",
|
"react-router-dom": "^7.8.2"
|
||||||
"sharp": "^0.34.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
@ -58,10 +34,8 @@
|
|||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/react": "^19.1.10",
|
"@types/react": "^19.1.10",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^19.1.7",
|
||||||
"@types/supertest": "^6.0.3",
|
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"concurrently": "^9.2.1",
|
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.33.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
@ -70,10 +44,7 @@
|
|||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"markdownlint": "^0.38.0",
|
"markdownlint": "^0.38.0",
|
||||||
"markdownlint-cli": "^0.45.0",
|
"markdownlint-cli": "^0.45.0",
|
||||||
"pdfjs-dist": "^4.8.69",
|
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"supertest": "^7.1.4",
|
|
||||||
"tesseract.js": "^5.1.0",
|
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.39.1",
|
"typescript-eslint": "^8.39.1",
|
||||||
"vite": "^7.1.2",
|
"vite": "^7.1.2",
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
import{j as s}from"./index-D2TD1aux.js";import{c as t}from"./Layout-lJ1qJP1d.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{j as s}from"./index-ChSrE95j.js";import{c as t}from"./Layout-EP4xiiHA.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{j as s}from"./index-CedKFzDs.js";import{c as t}from"./Layout-D-i60CyA.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{j as s}from"./index-BkuOAsyQ.js";import{c as t}from"./Layout-HUfQIbZm.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{j as s}from"./index-CuLfHvEh.js";import{c as t}from"./Layout-BwmFxbkK.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{j as s}from"./index-wde0U4qL.js";import{c as t}from"./Layout-aWSA1CnN.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{j as s}from"./index-DwyPw-ga.js";import{c as t}from"./Layout-BXCKqMhs.js";const o=t(s.jsx("path",{d:"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2M9 17H7v-7h2zm4 0h-2V7h2zm4 0h-2v-4h2z"}));export{o as A};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-DwyPw-ga.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-CedKFzDs.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-wde0U4qL.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-CuLfHvEh.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-BkuOAsyQ.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-D2TD1aux.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
import{b as i,a as l,r as d,d as p,j as u,s as m,c as f,h as x}from"./index-ChSrE95j.js";function g(t){return i("MuiCardContent",t)}l("MuiCardContent",["root"]);const y=t=>{const{classes:s}=t;return x({root:["root"]},g,s)},M=m("div",{name:"MuiCardContent",slot:"Root"})({padding:16,"&:last-child":{paddingBottom:24}}),U=d.forwardRef(function(s,o){const n=p({props:s,name:"MuiCardContent"}),{className:r,component:a="div",...C}=n,e={...n,component:a},c=y(e);return u.jsx(M,{as:a,className:f(c.root,r),ownerState:e,ref:o,...C})});export{U as C};
|
|
||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user