Compare commits
No commits in common. "dev" and "staging" 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
|
||||
VITE_API_URL=http://localhost:18000
|
||||
VITE_API_URL=http://localhost:8000
|
||||
|
||||
# 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_USE_RULE_NER=true
|
||||
VITE_LLM_CLASSIFY_ONLY=true
|
||||
|
||||
VITE_BACKEND_URL=http://localhost:3001
|
||||
|
||||
# Configuration des services externes (optionnel)
|
||||
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_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
|
||||
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
|
||||
VITE_API_URL=http://localhost:18000
|
||||
VITE_API_URL=http://localhost:8000
|
||||
|
||||
# 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_USE_RULE_NER=true
|
||||
VITE_LLM_CLASSIFY_ONLY=true
|
||||
|
||||
VITE_BACKEND_URL=http://localhost:3001
|
||||
|
||||
# Configuration des services externes (optionnel)
|
||||
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_BODACC_API_URL=https://bodacc-datadila.opendatasoft.com/api
|
||||
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*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.env
|
||||
.git
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
@ -23,40 +22,3 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.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
|
||||
- **Scope des entités limité** : Seules 4 entités sont maintenant retournées par l'API
|
||||
- `persons` : Identités des personnes
|
||||
- `companies` : Entreprises et sociétés
|
||||
- `addresses` : Adresses postales
|
||||
- `contractual` : Clauses et signatures contractuelles
|
||||
- **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 notariale complète** : Application front-end pour l'analyse de documents notariaux
|
||||
- **Upload de documents** : Glisser-déposer avec prévisualisation (PDF, images)
|
||||
- **Extraction de données** : OCR et identification d'objets standardisés
|
||||
- **Analyse intelligente** : Score de vraisemblance et recommandations
|
||||
- **Données contextuelles** : Intégration APIs externes (Cadastre, Géorisques, BODACC, Infogreffe)
|
||||
- **Conseil IA** : Analyse LLM avec détection de risques
|
||||
|
||||
#### Interface Utilisateur
|
||||
- **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
|
||||
### 🏗️ Architecture technique
|
||||
|
||||
### 🐛 Corrections
|
||||
- **Erreur React #31** : Correction des valeurs non-string passées aux composants React
|
||||
- **TypeScript strict** : Résolution des erreurs de compilation pour build stricte
|
||||
- **Déploiement** : Mise à jour automatique de l'interface sur `ai.4nkweb.com`
|
||||
- **React 18 + TypeScript** : Framework moderne avec typage strict
|
||||
- **Vite 7** : Build tool rapide et moderne
|
||||
- **Material-UI v6** : Interface utilisateur professionnelle
|
||||
- **Redux Toolkit** : Gestion d'état centralisée
|
||||
- **React Router v6** : Navigation avec code splitting
|
||||
- **Axios** : Client HTTP avec intercepteurs
|
||||
|
||||
### 📚 Documentation
|
||||
- **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
|
||||
### 🛠️ Outillage et qualité
|
||||
|
||||
---
|
||||
- **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é
|
||||
- **Détection des doublons** : Évite les fichiers identiques basés sur le contenu
|
||||
- **Cache JSON** : Sauvegarde automatique des résultats d'extraction
|
||||
- **Optimisation des performances** : Réutilisation des résultats en cache
|
||||
- **Mode démonstration** : Fonctionnement complet sans backend
|
||||
- **Gestion d'erreur gracieuse** : Fallback automatique pour tous les types d'erreurs
|
||||
- **Intercepteurs Axios** : Gestion centralisée des erreurs API
|
||||
- **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
|
||||
- `findExistingFileByHash(hash)` : Trouve les fichiers existants par hash
|
||||
- `saveJsonCache(hash, result)` : Sauvegarde un résultat dans le cache
|
||||
- `getJsonCache(hash)` : Récupère un résultat depuis le cache
|
||||
- `listCacheFiles()` : Liste tous les fichiers de cache
|
||||
- **Design professionnel** : Interface claire avec fond blanc
|
||||
- **Navigation intuitive** : Onglets et breadcrumbs
|
||||
- **Responsive** : Adaptation mobile et desktop
|
||||
- **Accessibilité** : Composants Material-UI accessibles
|
||||
|
||||
#### 📡 Nouvelles Routes API
|
||||
### 🚀 Déploiement et CI
|
||||
|
||||
- `GET /api/cache` : Liste les fichiers de cache avec métadonnées
|
||||
- `GET /api/cache/:hash` : Récupère un résultat de cache spécifique
|
||||
- `DELETE /api/cache/:hash` : Supprime un fichier de cache
|
||||
- `GET /api/uploads` : Liste les fichiers uploadés avec leurs hash
|
||||
- **Scripts npm** : Build, test, lint, format
|
||||
- **Variables d'environnement** : Configuration flexible des APIs
|
||||
- **Git workflow** : Branches dev, staging, release
|
||||
- **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`
|
||||
- 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
|
||||
### 📦 Dépendances principales
|
||||
|
||||
#### Configuration
|
||||
|
||||
- Ajout du dossier `cache/` au `.gitignore`
|
||||
- Configuration des remotes Git pour SSH/HTTPS
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
#### Nouveaux Fichiers
|
||||
|
||||
- `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_
|
||||
- `react@^18.3.1` - Framework UI
|
||||
- `typescript@^5.6.3` - Typage statique
|
||||
- `vite@^7.1.5` - Build tool
|
||||
- `@mui/material@^6.1.6` - Composants UI
|
||||
- `@reduxjs/toolkit@^2.3.0` - Gestion d'état
|
||||
- `react-router-dom@^6.28.0` - Routing
|
||||
- `axios@^1.7.7` - Client HTTP
|
||||
- `vitest@^2.1.8` - Framework de test
|
||||
|
||||
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.
|
||||
|
||||
@ -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)
|
||||
- **Prévisualisation** : Affichage des documents uploadés
|
||||
- **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
|
||||
|
||||
@ -22,7 +19,9 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
|
||||
|
||||
### 📊 Analyse intelligente
|
||||
|
||||
- **Score de vraisemblance** : Évaluation de la crédibilité du document
|
||||
- **Recommandations** : Suggestions d'actions à effectuer
|
||||
- **Synthèse** : Résumé automatique du document
|
||||
|
||||
### 🌐 Données contextuelles
|
||||
|
||||
@ -39,38 +38,26 @@ Application front-end pour l'analyse intelligente de documents notariaux avec IA
|
||||
|
||||
## 🚀 Technologies
|
||||
|
||||
- **Frontend** : React 19 + TypeScript
|
||||
- **Frontend** : React 18 + TypeScript
|
||||
- **Build** : Vite 7
|
||||
- **UI** : Material-UI (MUI) v7
|
||||
- **UI** : Material-UI (MUI) v6
|
||||
- **State** : Redux Toolkit + React Redux
|
||||
- **Routing** : React Router v7
|
||||
- **Routing** : React Router v6
|
||||
- **HTTP** : Axios
|
||||
- **Tests** : Vitest + Testing Library
|
||||
- **Linting** : ESLint + Prettier + markdownlint
|
||||
- **Backend** : Node.js + Express
|
||||
- **OCR** : Tesseract.js
|
||||
- **IA** : OpenAI API
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### 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
|
||||
- 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
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm install
|
||||
```
|
||||
|
||||
### Configuration des environnements
|
||||
@ -79,11 +66,6 @@ Créer un fichier `.env` :
|
||||
|
||||
```env
|
||||
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_GEORISQUES_API_URL=https://www.georisques.gouv.fr/api
|
||||
VITE_GEOFONCIER_API_URL=https://api.geofoncier.fr
|
||||
@ -95,37 +77,21 @@ VITE_INFOGREFFE_API_URL=https://api.infogreffe.fr
|
||||
|
||||
```bash
|
||||
# Développement
|
||||
npm run start:all # Démarrer backend (PM2) + frontend (Vite) ensemble
|
||||
npm run start:dev # Clean complet, backend restart, front Vite (séquentiel)
|
||||
npm run dev # Serveur de développement (frontend seul)
|
||||
npm run dev # Serveur de développement
|
||||
npm run build # Build de production
|
||||
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
|
||||
npm run lint # Vérification ESLint
|
||||
npm run lint:fix # Correction automatique ESLint
|
||||
npm run format # Vérification Prettier
|
||||
npm run format:fix # Formatage automatique
|
||||
npm run mdlint # Vérification Markdown
|
||||
|
||||
# Tests
|
||||
npm run test # Tests unitaires avec couverture
|
||||
npm run test # Tests unitaires
|
||||
npm run test:ui # Tests avec interface
|
||||
|
||||
# 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
|
||||
npm run test:coverage # Tests avec couverture
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
@ -140,6 +106,7 @@ src/
|
||||
├── views/ # Vues de l'application
|
||||
│ ├── UploadView.tsx # Upload de documents
|
||||
│ ├── ExtractionView.tsx # Extraction de données
|
||||
│ ├── AnalyseView.tsx # Analyse des documents
|
||||
│ ├── ContexteView.tsx # Données contextuelles
|
||||
│ └── ConseilView.tsx # Conseil IA
|
||||
├── store/ # Gestion d'état Redux
|
||||
@ -180,10 +147,14 @@ src/
|
||||
- **Breadcrumbs** : Indication de la position actuelle
|
||||
- **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`.
|
||||
Les APIs externes (Cadastre, Géorisques, Géofoncier, BODACC, Infogreffe) sont appelées côté backend uniquement.
|
||||
L'application fonctionne parfaitement en mode démonstration :
|
||||
|
||||
- **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
|
||||
|
||||
@ -217,19 +188,6 @@ npm run test:coverage # Rapport de couverture
|
||||
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
|
||||
|
||||
Configurer les URLs des APIs externes selon l'environnement :
|
||||
@ -275,5 +233,5 @@ Pour toute question ou problème :
|
||||
|
||||
---
|
||||
|
||||
**Version actuelle** : 0.1.3
|
||||
**Dernière mise à jour** : Septembre 2025
|
||||
**Version actuelle** : 0.1.0
|
||||
**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
|
||||
|
||||
L'application IA Lecoffre.io communique uniquement avec le backend interne pour toutes les
|
||||
fonctionnalités (upload, extraction, analyse, contexte, conseil).
|
||||
L'application 4NK IA Front Notarial communique avec plusieurs APIs pour fournir une expérience complète
|
||||
d'analyse de documents notariaux.
|
||||
|
||||
## API Backend Principal
|
||||
|
||||
@ -18,41 +18,86 @@ http://localhost:8000 (développement)
|
||||
#### Upload de document
|
||||
|
||||
```http
|
||||
POST /api/notary/upload
|
||||
POST /api/documents/upload
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
Body: FormData avec le fichier
|
||||
```
|
||||
|
||||
Réponse attendue (champs utilisés par le front) :
|
||||
|
||||
```json
|
||||
{
|
||||
"document_id": "doc_123456",
|
||||
"mime_type": "application/pdf",
|
||||
"functional_type": "CNI"
|
||||
}
|
||||
```
|
||||
|
||||
Mappage front en `Document` :
|
||||
**Réponse :**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "doc_123456",
|
||||
"name": "acte_vente.pdf",
|
||||
"mimeType": "application/pdf",
|
||||
"functionalType": "CNI",
|
||||
"type": "application/pdf",
|
||||
"size": 1024000,
|
||||
"uploadDate": "<date locale>",
|
||||
"status": "completed",
|
||||
"previewUrl": "blob:..."
|
||||
"uploadDate": "2024-01-15T10:30:00Z",
|
||||
"status": "completed"
|
||||
}
|
||||
```
|
||||
|
||||
#### Extraction de données
|
||||
|
||||
```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
|
||||
@ -61,38 +106,128 @@ GET /api/notary/documents/{documentId}
|
||||
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
|
||||
|
||||
```http
|
||||
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
|
||||
|
||||
```http
|
||||
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
|
||||
|
||||
Les APIs externes (Cadastre, Géorisques, Géofoncier, BODACC, Infogreffe) sont appelées côté backend
|
||||
uniquement. Aucun appel direct côté front.
|
||||
### Cadastre
|
||||
|
||||
**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
|
||||
|
||||
### Codes d'erreur HTTP
|
||||
|
||||
- 200 : Succès
|
||||
- 400 : Requête malformée
|
||||
- 404 : Ressource non trouvée
|
||||
- 405 : Méthode non autorisée
|
||||
- 500 : Erreur serveur interne
|
||||
- **200** : Succès
|
||||
- **400** : Requête malformée
|
||||
- **404** : Ressource non trouvée
|
||||
- **405** : Méthode non autorisée
|
||||
- **500** : Erreur serveur interne
|
||||
|
||||
### Erreurs de connexion
|
||||
|
||||
- ERR_NETWORK : Erreur de réseau
|
||||
- ERR_CONNECTION_REFUSED : Connexion refusée
|
||||
- ERR_TIMEOUT : Timeout de la requête
|
||||
- **ERR_NETWORK** : Erreur de réseau
|
||||
- **ERR_CONNECTION_REFUSED** : Connexion refusée
|
||||
- **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
|
||||
|
||||
@ -100,28 +235,22 @@ uniquement. Aucun appel direct côté front.
|
||||
|
||||
```env
|
||||
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_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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
```typescript
|
||||
const apiClient = axios.create({
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
- 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 :
|
||||
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
|
||||
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
|
||||
4. Pour les identité : rechercher des informations générales sur la personne
|
||||
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
|
||||
Sites et sols pollués (SSP) 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
|
||||
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
||||
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||
Zonage Sismique
|
||||
Opérations sur le risque sismique
|
||||
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/)
|
||||
Infogreffe
|
||||
[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/)
|
||||
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
||||
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
|
||||
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
|
||||
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
|
||||
4. Pour les identité : rechercher des informations générales sur la personne
|
||||
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
|
||||
Sites et sols pollués (SSP) 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
|
||||
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
||||
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||
Zonage Sismique
|
||||
Opérations sur le risque sismique
|
||||
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/)
|
||||
Infogreffe
|
||||
[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/)
|
||||
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
||||
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 :
|
||||
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
|
||||
@ -165,7 +167,7 @@ faire une api et une une ihm qui les consomme pour:
|
||||
|
||||
1. Détecter un type de document
|
||||
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
|
||||
4. Pour les identité : rechercher des informations générales sur la personne
|
||||
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
|
||||
Sites et sols pollués (SSP) 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
|
||||
Opérations sur les Territoires <20> Risques importants d'Inondation (TRI)
|
||||
Opérations sur les Territoires à Risques importants d'Inondation (TRI)
|
||||
Zonage Sismique
|
||||
Opérations sur le risque sismique
|
||||
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/)
|
||||
Infogreffe
|
||||
[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/)
|
||||
faire demande [https://data.inpi.fr/content/editorial/acces_BE](https://data.inpi.fr/content/editorial/acces_BE)
|
||||
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
|
||||
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'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist', 'coverage', 'backend/node_modules', 'node_modules']),
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
@ -19,14 +19,5 @@ export default tseslint.config([
|
||||
ecmaVersion: 2020,
|
||||
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" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>IA - Lecoffre.io</title>
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<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",
|
||||
"private": true,
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "node scripts/check-node.mjs",
|
||||
"dev": "vite",
|
||||
"prebuild": "node scripts/check-node.mjs",
|
||||
"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 .",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"mdlint": "markdownlint . --ignore node_modules --ignore dist",
|
||||
"test": "vitest run --coverage",
|
||||
"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"
|
||||
"test:ui": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
@ -39,17 +21,11 @@
|
||||
"@mui/material": "^7.3.2",
|
||||
"@reduxjs/toolkit": "^2.9.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-dom": "^19.1.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.9.1",
|
||||
"sharp": "^0.34.3"
|
||||
"react-router-dom": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
@ -58,10 +34,8 @@
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
@ -70,10 +44,7 @@
|
||||
"jsdom": "^26.1.0",
|
||||
"markdownlint": "^0.38.0",
|
||||
"markdownlint-cli": "^0.45.0",
|
||||
"pdfjs-dist": "^4.8.69",
|
||||
"prettier": "^3.6.2",
|
||||
"supertest": "^7.1.4",
|
||||
"tesseract.js": "^5.1.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"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