Initial commit

This commit is contained in:
Dom
2026-03-05 01:20:15 +01:00
commit c0c50e56f0
364 changed files with 62207 additions and 0 deletions

447
omop/frontend/src/App.css Normal file
View File

@@ -0,0 +1,447 @@
.app {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 250px;
background: #2c3e50;
color: white;
padding: 20px;
position: fixed;
height: 100vh;
overflow-y: auto;
}
.logo h2 {
margin-bottom: 30px;
font-size: 24px;
border-bottom: 2px solid #3498db;
padding-bottom: 15px;
}
.nav-links {
list-style: none;
}
.nav-links li {
margin-bottom: 10px;
}
.nav-links a {
color: #ecf0f1;
text-decoration: none;
display: block;
padding: 12px 15px;
border-radius: 5px;
transition: all 0.3s;
font-size: 16px;
}
.nav-links a:hover {
background: #34495e;
transform: translateX(5px);
}
.main-content {
margin-left: 250px;
flex: 1;
padding: 30px;
width: calc(100% - 250px);
}
.page-header {
margin-bottom: 30px;
}
.page-header h1 {
font-size: 32px;
color: #2c3e50;
margin-bottom: 10px;
}
.page-header p {
color: #7f8c8d;
font-size: 16px;
}
.card {
background: white;
border-radius: 8px;
padding: 25px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.card h2 {
font-size: 20px;
color: #2c3e50;
margin-bottom: 15px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-left: 4px solid #3498db;
}
.stat-card.success {
border-left-color: #27ae60;
}
.stat-card.warning {
border-left-color: #f39c12;
}
.stat-card.error {
border-left-color: #e74c3c;
}
.stat-card h3 {
font-size: 14px;
color: #7f8c8d;
margin-bottom: 10px;
text-transform: uppercase;
}
.stat-card .value {
font-size: 32px;
font-weight: bold;
color: #2c3e50;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.btn-success {
background: #27ae60;
color: white;
}
.btn-success:hover {
background: #229954;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #2c3e50;
font-weight: 500;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #3498db;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ecf0f1;
}
.table th {
background: #f8f9fa;
color: #2c3e50;
font-weight: 600;
}
.table tr:hover {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.badge-success {
background: #d4edda;
color: #155724;
}
.badge-warning {
background: #fff3cd;
color: #856404;
}
.badge-error {
background: #f8d7da;
color: #721c24;
}
.badge-info {
background: #d1ecf1;
color: #0c5460;
}
.loading {
text-align: center;
padding: 40px;
color: #7f8c8d;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
/* Documentation Page Styles */
.documentation-page {
max-width: 100%;
}
.doc-layout {
display: flex;
gap: 30px;
margin-top: 20px;
}
.doc-sidebar {
width: 250px;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 20px;
height: fit-content;
}
.doc-sidebar h3 {
font-size: 16px;
color: #2c3e50;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.doc-nav {
display: flex;
flex-direction: column;
gap: 5px;
}
.doc-nav-item {
background: transparent;
border: none;
padding: 12px 15px;
text-align: left;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
color: #7f8c8d;
font-size: 14px;
font-weight: 500;
}
.doc-nav-item:hover {
background: #f8f9fa;
color: #2c3e50;
}
.doc-nav-item.active {
background: #3498db;
color: white;
}
.doc-content {
flex: 1;
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 900px;
}
.doc-content h2 {
font-size: 28px;
color: #2c3e50;
margin-bottom: 20px;
border-bottom: 3px solid #3498db;
padding-bottom: 10px;
}
.doc-content h3 {
font-size: 22px;
color: #2c3e50;
margin-top: 25px;
margin-bottom: 15px;
}
.doc-content h4 {
font-size: 18px;
color: #34495e;
margin-top: 20px;
margin-bottom: 10px;
}
.doc-content p {
line-height: 1.8;
color: #555;
margin-bottom: 15px;
}
.doc-content ul,
.doc-content ol {
line-height: 1.8;
color: #555;
margin-bottom: 15px;
padding-left: 25px;
}
.doc-content li {
margin-bottom: 8px;
}
.doc-content code {
background: #f8f9fa;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #e74c3c;
}
.doc-content strong {
color: #2c3e50;
font-weight: 600;
}
.doc-card {
background: #f8f9fa;
border-left: 4px solid #3498db;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
.doc-card h3 {
margin-top: 0;
color: #3498db;
}
.doc-card h4 {
margin-top: 15px;
color: #2c3e50;
}
.doc-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.doc-table th,
.doc-table td {
padding: 12px;
text-align: left;
border: 1px solid #ddd;
}
.doc-table th {
background: #3498db;
color: white;
font-weight: 600;
}
.doc-table tr:nth-child(even) {
background: #f8f9fa;
}
.glossary {
margin: 0;
}
.glossary dt {
font-weight: 600;
color: #2c3e50;
margin-top: 15px;
margin-bottom: 5px;
font-size: 16px;
}
.glossary dd {
margin-left: 20px;
color: #555;
line-height: 1.6;
padding-bottom: 10px;
border-bottom: 1px solid #ecf0f1;
}
/* Responsive adjustments */
@media (max-width: 1024px) {
.doc-layout {
flex-direction: column;
}
.doc-sidebar {
width: 100%;
position: static;
}
.doc-nav {
flex-direction: row;
flex-wrap: wrap;
}
}

44
omop/frontend/src/App.jsx Normal file
View File

@@ -0,0 +1,44 @@
import React from 'react'
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
import Dashboard from './pages/Dashboard'
import ETLManager from './pages/ETLManager'
import SchemaManager from './pages/SchemaManager'
import Validation from './pages/Validation'
import Logs from './pages/Logs'
import Documentation from './pages/Documentation'
import './App.css'
function App() {
return (
<BrowserRouter>
<div className="app">
<nav className="sidebar">
<div className="logo">
<h2>OMOP Pipeline</h2>
</div>
<ul className="nav-links">
<li><Link to="/">📊 Dashboard</Link></li>
<li><Link to="/etl"> ETL Manager</Link></li>
<li><Link to="/schema">🗄 Schema</Link></li>
<li><Link to="/validation"> Validation</Link></li>
<li><Link to="/logs">📝 Logs</Link></li>
<li><Link to="/documentation">📖 Documentation</Link></li>
</ul>
</nav>
<main className="main-content">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/etl" element={<ETLManager />} />
<Route path="/schema" element={<SchemaManager />} />
<Route path="/validation" element={<Validation />} />
<Route path="/logs" element={<Logs />} />
<Route path="/documentation" element={<Documentation />} />
</Routes>
</main>
</div>
</BrowserRouter>
)
}
export default App

View File

@@ -0,0 +1,53 @@
import axios from 'axios'
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001/api'
const client = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
}
})
export const api = {
// ETL endpoints
etl: {
run: (data) => client.post('/etl/run', data),
getJob: (jobId) => client.get(`/etl/jobs/${jobId}`),
listJobs: () => client.get('/etl/jobs'),
extract: (sourceTable, batchSize) =>
client.post('/etl/extract', null, { params: { source_table: sourceTable, batch_size: batchSize } }),
transform: (targetTable) =>
client.post('/etl/transform', null, { params: { target_table: targetTable } }),
load: (targetTable) =>
client.post('/etl/load', null, { params: { target_table: targetTable } })
},
// Schema endpoints
schema: {
create: (schemaType) => client.post('/schema/create', { schema_type: schemaType }),
validate: () => client.get('/schema/validate'),
info: () => client.get('/schema/info')
},
// Stats endpoints
stats: {
etl: (limit) => client.get('/stats/etl', { params: { limit } }),
dataQuality: () => client.get('/stats/data-quality'),
summary: () => client.get('/stats/summary')
},
// Validation endpoints
validation: {
run: (tableName) => client.post('/validation/run', null, { params: { table_name: tableName } }),
unmappedCodes: (limit) => client.get('/validation/unmapped-codes', { params: { limit } })
},
// Logs endpoints
logs: {
get: (lines, level) => client.get('/logs/', { params: { lines, level } }),
errors: (limit) => client.get('/logs/errors', { params: { limit } })
}
}
export default client

View File

@@ -0,0 +1,28 @@
import React from 'react'
import Tooltip from './Tooltip'
function HelpIcon({ text }) {
return (
<Tooltip text={text}>
<span style={{
display: 'inline-block',
width: '18px',
height: '18px',
borderRadius: '50%',
background: '#3498db',
color: 'white',
fontSize: '12px',
fontWeight: 'bold',
textAlign: 'center',
lineHeight: '18px',
cursor: 'help',
marginLeft: '6px',
verticalAlign: 'middle'
}}>
?
</span>
</Tooltip>
)
}
export default HelpIcon

View File

@@ -0,0 +1,50 @@
import React, { useState } from 'react'
function Tooltip({ text, children }) {
const [show, setShow] = useState(false)
return (
<span
style={{ position: 'relative', display: 'inline-block' }}
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
>
{children}
{show && (
<div style={{
position: 'absolute',
bottom: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginBottom: '8px',
padding: '8px 12px',
background: '#2c3e50',
color: 'white',
borderRadius: '6px',
fontSize: '13px',
whiteSpace: 'nowrap',
zIndex: 1000,
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
maxWidth: '300px',
whiteSpace: 'normal',
textAlign: 'center'
}}>
{text}
<div style={{
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
width: 0,
height: 0,
borderLeft: '6px solid transparent',
borderRight: '6px solid transparent',
borderTop: '6px solid #2c3e50'
}} />
</div>
)}
</span>
)
}
export default Tooltip

View File

@@ -0,0 +1,18 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #f5f7fa;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}

View File

@@ -0,0 +1,15 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'
import './index.css'
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
)

View File

@@ -0,0 +1,127 @@
import React from 'react'
import { useQuery } from '@tanstack/react-query'
import { api } from '../api/client'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
import HelpIcon from '../components/HelpIcon'
function Dashboard() {
const { data: summary, isLoading: summaryLoading } = useQuery({
queryKey: ['summary'],
queryFn: () => api.stats.summary().then(res => res.data),
refetchInterval: 5000
})
const { data: etlStats, isLoading: etlLoading } = useQuery({
queryKey: ['etl-stats'],
queryFn: () => api.stats.etl(10).then(res => res.data),
refetchInterval: 5000
})
if (summaryLoading || etlLoading) {
return <div className="loading">Chargement...</div>
}
return (
<div>
<div className="page-header">
<h1>
Dashboard OMOP Pipeline
<HelpIcon text="Vue d'ensemble en temps réel de votre pipeline de données OMOP CDM. Suivez les statistiques des tables, les exécutions ETL et l'état général du système." />
</h1>
<p>Vue d'ensemble du système ETL</p>
</div>
<div className="stats-grid">
<div className="stat-card success">
<h3>
Patients OMOP
<HelpIcon text="Nombre total de patients dans la table OMOP 'person'. Ces données ont été transformées et validées selon le standard OMOP CDM 5.4." />
</h3>
<div className="value">{summary?.summary?.omop_records?.person || 0}</div>
</div>
<div className="stat-card">
<h3>
Visites
<HelpIcon text="Nombre de visites médicales enregistrées dans 'visit_occurrence'. Chaque visite représente une interaction patient-établissement de santé." />
</h3>
<div className="value">{summary?.summary?.omop_records?.visit_occurrence || 0}</div>
</div>
<div className="stat-card">
<h3>
Conditions
<HelpIcon text="Nombre de diagnostics/conditions médicales dans 'condition_occurrence'. Inclut les maladies, symptômes et diagnostics des patients." />
</h3>
<div className="value">{summary?.summary?.omop_records?.condition_occurrence || 0}</div>
</div>
<div className="stat-card warning">
<h3>
En attente
<HelpIcon text="Nombre d'enregistrements dans les tables de staging avec le statut 'pending'. Ces données attendent d'être traitées par le pipeline ETL." />
</h3>
<div className="value">{summary?.summary?.staging_pending || 0}</div>
</div>
</div>
<div className="card">
<h2>
Exécutions récentes (24h)
<HelpIcon text="Statistiques des pipelines ETL exécutés dans les dernières 24 heures. Permet de suivre le taux de succès et d'identifier les problèmes." />
</h2>
<div className="stats-grid">
<div className="stat-card">
<h3>Total</h3>
<div className="value">{summary?.summary?.executions_24h?.total || 0}</div>
</div>
<div className="stat-card success">
<h3>Réussies</h3>
<div className="value">{summary?.summary?.executions_24h?.completed || 0}</div>
</div>
<div className="stat-card error">
<h3>Échouées</h3>
<div className="value">{summary?.summary?.executions_24h?.failed || 0}</div>
</div>
</div>
</div>
<div className="card">
<h2>
Historique ETL
<HelpIcon text="Liste détaillée des 10 dernières exécutions ETL avec leur statut, nombre d'enregistrements traités et durée d'exécution." />
</h2>
<table className="table">
<thead>
<tr>
<th>Pipeline</th>
<th>Début</th>
<th>Statut</th>
<th>Enregistrements</th>
<th>Échecs</th>
<th>Durée (s)</th>
</tr>
</thead>
<tbody>
{etlStats?.stats?.map((stat, idx) => (
<tr key={idx}>
<td>{stat.pipeline_name}</td>
<td>{new Date(stat.start_time).toLocaleString('fr-FR')}</td>
<td>
<span className={`badge badge-${stat.status === 'completed' ? 'success' : stat.status === 'failed' ? 'error' : 'warning'}`}>
{stat.status}
</span>
</td>
<td>{stat.records_processed}</td>
<td>{stat.records_failed}</td>
<td>{stat.duration_seconds?.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
export default Dashboard

View File

@@ -0,0 +1,423 @@
import React, { useState } from 'react'
import HelpIcon from '../components/HelpIcon'
function Documentation() {
const [activeSection, setActiveSection] = useState('overview')
const sections = {
overview: {
title: '📖 Vue d\'ensemble',
content: (
<>
<h2>Bienvenue dans OMOP Pipeline</h2>
<p>
Cette application vous permet de transformer vos données de santé brutes en format
<strong> OMOP CDM 5.4</strong> (Observational Medical Outcomes Partnership Common Data Model).
</p>
<div className="doc-card">
<h3>🎯 Objectif</h3>
<p>
Le pipeline OMOP standardise vos données de santé pour permettre des analyses
interopérables et des études observationnelles à grande échelle.
</p>
</div>
<div className="doc-card">
<h3>🔄 Workflow Général</h3>
<ol>
<li><strong>Staging</strong> : Chargement des données brutes</li>
<li><strong>ETL</strong> : Transformation au format OMOP</li>
<li><strong>Validation</strong> : Vérification de la qualité</li>
<li><strong>Exploitation</strong> : Analyses et requêtes</li>
</ol>
</div>
<div className="doc-card">
<h3>📊 Architecture</h3>
<ul>
<li><strong>Schéma OMOP</strong> : Tables standardisées (person, visit_occurrence, etc.)</li>
<li><strong>Schéma Staging</strong> : Tables temporaires pour données brutes</li>
<li><strong>Schéma Audit</strong> : Logs et traçabilité des transformations</li>
</ul>
</div>
</>
)
},
etl: {
title: '⚙️ ETL (Extract-Transform-Load)',
content: (
<>
<h2>Processus ETL</h2>
<p>
<strong>ETL</strong> signifie Extract-Transform-Load (Extraire-Transformer-Charger).
C'est le cœur du pipeline OMOP.
</p>
<div className="doc-card">
<h3>1⃣ Extract (Extraction)</h3>
<p>
Les données sont extraites des tables de staging où elles ont été chargées
depuis vos sources (fichiers CSV, bases de données, APIs, etc.).
</p>
<ul>
<li>Tables source : <code>staging.raw_patients</code>, <code>staging.raw_visits</code>, etc.</li>
<li>Seuls les enregistrements avec <code>status='pending'</code> sont traités</li>
<li>Traitement par lots (batch) pour optimiser les performances</li>
</ul>
</div>
<div className="doc-card">
<h3>2⃣ Transform (Transformation)</h3>
<p>
Les données sont transformées pour correspondre au modèle OMOP CDM 5.4 :
</p>
<ul>
<li><strong>Mapping des codes</strong> : Conversion vers vocabulaires OMOP (SNOMED, ICD10, etc.)</li>
<li><strong>Normalisation</strong> : Formats de dates, types de données, unités</li>
<li><strong>Enrichissement</strong> : Ajout de métadonnées et références</li>
<li><strong>Validation</strong> : Vérification des contraintes et règles métier</li>
</ul>
</div>
<div className="doc-card">
<h3>3⃣ Load (Chargement)</h3>
<p>
Les données transformées sont chargées dans les tables OMOP finales :
</p>
<ul>
<li><code>person</code> : Informations démographiques des patients</li>
<li><code>visit_occurrence</code> : Visites et séjours hospitaliers</li>
<li><code>condition_occurrence</code> : Diagnostics et conditions médicales</li>
<li><code>drug_exposure</code> : Prescriptions et administrations médicamenteuses</li>
</ul>
</div>
<div className="doc-card">
<h3>⚡ Paramètres de Performance</h3>
<table className="doc-table">
<thead>
<tr>
<th>Paramètre</th>
<th>Description</th>
<th>Recommandation</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Batch Size</strong></td>
<td>Nombre d'enregistrements par lot</td>
<td>1000-5000 (selon RAM disponible)</td>
</tr>
<tr>
<td><strong>Workers</strong></td>
<td>Processus parallèles</td>
<td>4-8 (selon CPU disponibles)</td>
</tr>
<tr>
<td><strong>Mode séquentiel</strong></td>
<td>Désactive la parallélisation</td>
<td>Uniquement pour débogage</td>
</tr>
</tbody>
</table>
</div>
</>
)
},
schemas: {
title: '🗄️ Schémas de Base de Données',
content: (
<>
<h2>Architecture des Schémas</h2>
<div className="doc-card">
<h3>📦 Schéma OMOP</h3>
<p>
Contient les tables standardisées selon OMOP CDM 5.4. C'est le schéma principal
pour vos analyses.
</p>
<h4>Tables principales :</h4>
<ul>
<li><code>person</code> : Patients (démographie, genre, année de naissance)</li>
<li><code>visit_occurrence</code> : Visites médicales et hospitalisations</li>
<li><code>condition_occurrence</code> : Diagnostics et conditions</li>
<li><code>drug_exposure</code> : Prescriptions médicamenteuses</li>
<li><code>procedure_occurrence</code> : Actes et procédures médicales</li>
<li><code>measurement</code> : Mesures et résultats de laboratoire</li>
<li><code>observation</code> : Observations cliniques diverses</li>
</ul>
</div>
<div className="doc-card">
<h3>📥 Schéma Staging</h3>
<p>
Zone de transit pour les données brutes avant transformation. Les données
y sont chargées depuis vos sources externes.
</p>
<h4>Tables de staging :</h4>
<ul>
<li><code>raw_patients</code> : Données patients brutes</li>
<li><code>raw_visits</code> : Données de visites brutes</li>
<li><code>raw_conditions</code> : Diagnostics bruts</li>
<li><code>raw_drugs</code> : Prescriptions brutes</li>
</ul>
<p>
Chaque enregistrement a un <code>status</code> :
<span className="badge badge-warning">pending</span>,
<span className="badge badge-success">processed</span>, ou
<span className="badge badge-error">failed</span>
</p>
</div>
<div className="doc-card">
<h3>📝 Schéma Audit</h3>
<p>
Traçabilité complète des transformations ETL pour conformité et débogage.
</p>
<h4>Tables d'audit :</h4>
<ul>
<li><code>etl_execution</code> : Historique des exécutions ETL</li>
<li><code>etl_execution_stats</code> : Statistiques détaillées par exécution</li>
<li><code>data_quality_errors</code> : Erreurs de validation détectées</li>
<li><code>unmapped_codes</code> : Codes sources sans mapping OMOP</li>
</ul>
</div>
</>
)
},
validation: {
title: '✅ Validation et Qualité',
content: (
<>
<h2>Validation des Données</h2>
<div className="doc-card">
<h3>🎯 Objectifs de la Validation</h3>
<ul>
<li>Vérifier la conformité au standard OMOP CDM 5.4</li>
<li>Détecter les erreurs de transformation</li>
<li>Identifier les codes non mappés</li>
<li>Assurer l'intégrité référentielle</li>
<li>Valider les contraintes métier</li>
</ul>
</div>
<div className="doc-card">
<h3>🔍 Types de Validation</h3>
<h4>1. Validation Structurelle</h4>
<ul>
<li>Présence des champs obligatoires</li>
<li>Types de données corrects</li>
<li>Formats de dates valides</li>
<li>Valeurs dans les plages autorisées</li>
</ul>
<h4>2. Validation Référentielle</h4>
<ul>
<li>Existence des patients référencés</li>
<li>Cohérence des dates (visite avant diagnostic, etc.)</li>
<li>Validité des codes dans les vocabulaires OMOP</li>
</ul>
<h4>3. Validation Métier</h4>
<ul>
<li>Âge cohérent avec l'année de naissance</li>
<li>Genre compatible avec les conditions</li>
<li>Durées de séjour réalistes</li>
<li>Dosages médicamenteux dans les normes</li>
</ul>
</div>
<div className="doc-card">
<h3> Codes Non Mappés</h3>
<p>
Les codes non mappés sont des codes sources (ICD10, CIM10, etc.) qui n'ont pas
de correspondance dans les vocabulaires OMOP standard.
</p>
<h4>Actions recommandées :</h4>
<ol>
<li>Vérifier si le code existe dans le vocabulaire source</li>
<li>Chercher un code équivalent ou parent</li>
<li>Créer un mapping personnalisé si nécessaire</li>
<li>Documenter les codes non mappables</li>
</ol>
</div>
</>
)
},
glossary: {
title: '📚 Glossaire',
content: (
<>
<h2>Glossaire des Termes</h2>
<div className="doc-card">
<h3>A-E</h3>
<dl className="glossary">
<dt>Audit</dt>
<dd>Traçabilité des transformations et modifications de données</dd>
<dt>Batch</dt>
<dd>Lot d'enregistrements traités ensemble pour optimiser les performances</dd>
<dt>CDM (Common Data Model)</dt>
<dd>Modèle de données commun standardisé par OHDSI</dd>
<dt>Concept</dt>
<dd>Terme standardisé dans un vocabulaire OMOP (maladie, médicament, etc.)</dd>
<dt>ETL</dt>
<dd>Extract-Transform-Load : processus de transformation des données</dd>
</dl>
</div>
<div className="doc-card">
<h3>M-S</h3>
<dl className="glossary">
<dt>Mapping</dt>
<dd>Correspondance entre un code source et un concept OMOP standard</dd>
<dt>OHDSI</dt>
<dd>Observational Health Data Sciences and Informatics (consortium international)</dd>
<dt>OMOP</dt>
<dd>Observational Medical Outcomes Partnership</dd>
<dt>Pipeline</dt>
<dd>Chaîne de traitement automatisée des données</dd>
<dt>Staging</dt>
<dd>Zone temporaire de stockage des données brutes avant transformation</dd>
</dl>
</div>
<div className="doc-card">
<h3>V-W</h3>
<dl className="glossary">
<dt>Vocabulaire</dt>
<dd>Ensemble standardisé de termes médicaux (SNOMED, ICD10, RxNorm, etc.)</dd>
<dt>Worker</dt>
<dd>Processus parallèle qui traite une partie des données</dd>
</dl>
</div>
</>
)
},
faq: {
title: '❓ FAQ',
content: (
<>
<h2>Questions Fréquentes</h2>
<div className="doc-card">
<h3>🚀 Démarrage</h3>
<h4>Comment démarrer avec OMOP Pipeline ?</h4>
<ol>
<li>Créez les schémas (page Schema Manager)</li>
<li>Chargez vos données brutes dans les tables staging</li>
<li>Lancez un pipeline ETL (page ETL Manager)</li>
<li>Validez les résultats (page Validation)</li>
</ol>
<h4>Mes données sont-elles sécurisées ?</h4>
<p>
Oui. Les données restent dans votre base PostgreSQL locale. Aucune donnée
n'est envoyée à l'extérieur. Assurez-vous de sécuriser votre base de données
selon vos politiques de sécurité.
</p>
</div>
<div className="doc-card">
<h3> ETL</h3>
<h4>Combien de temps prend un pipeline ETL ?</h4>
<p>
Cela dépend du volume de données et des paramètres :
</p>
<ul>
<li>100 patients : ~10-30 secondes</li>
<li>1000 patients : ~1-3 minutes</li>
<li>10000 patients : ~10-30 minutes</li>
</ul>
<h4>Que faire si un pipeline échoue ?</h4>
<ol>
<li>Consultez les logs (page Logs)</li>
<li>Vérifiez les erreurs de validation</li>
<li>Corrigez les données sources si nécessaire</li>
<li>Relancez le pipeline</li>
</ol>
<h4>Puis-je relancer un pipeline sur les mêmes données ?</h4>
<p>
Oui, mais seuls les enregistrements avec <code>status='pending'</code> seront
traités. Les enregistrements déjà traités sont ignorés.
</p>
</div>
<div className="doc-card">
<h3>📊 Données</h3>
<h4>Pourquoi ai-je des codes non mappés ?</h4>
<p>
Les codes non mappés apparaissent quand un code source n'a pas de correspondance
dans les vocabulaires OMOP. Cela peut arriver si :
</p>
<ul>
<li>Le code est obsolète ou incorrect</li>
<li>Le vocabulaire OMOP n'est pas à jour</li>
<li>Un mapping personnalisé est nécessaire</li>
</ul>
<h4>Comment améliorer la qualité de mes données ?</h4>
<ol>
<li>Utilisez la page Validation régulièrement</li>
<li>Corrigez les codes non mappés</li>
<li>Vérifiez les erreurs dans les logs</li>
<li>Assurez-vous que vos données sources sont complètes</li>
</ol>
</div>
</>
)
}
}
return (
<div className="documentation-page">
<div className="page-header">
<h1>
📖 Documentation
<HelpIcon text="Documentation complète de l'application OMOP Pipeline. Consultez les guides, le glossaire et les FAQ pour maîtriser l'outil." />
</h1>
<p>Guide complet d'utilisation de OMOP Pipeline</p>
</div>
<div className="doc-layout">
<aside className="doc-sidebar">
<h3>Sections</h3>
<nav className="doc-nav">
{Object.entries(sections).map(([key, section]) => (
<button
key={key}
className={`doc-nav-item ${activeSection === key ? 'active' : ''}`}
onClick={() => setActiveSection(key)}
>
{section.title}
</button>
))}
</nav>
</aside>
<main className="doc-content">
{sections[activeSection].content}
</main>
</div>
</div>
)
}
export default Documentation

View File

@@ -0,0 +1,175 @@
import React, { useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { api } from '../api/client'
import HelpIcon from '../components/HelpIcon'
function ETLManager() {
const queryClient = useQueryClient()
const [formData, setFormData] = useState({
source_table: 'staging.raw_patients',
target_table: 'person',
batch_size: 1000,
num_workers: 8,
sequential: false
})
const { data: jobs } = useQuery({
queryKey: ['etl-jobs'],
queryFn: () => api.etl.listJobs().then(res => res.data),
refetchInterval: 2000
})
const runMutation = useMutation({
mutationFn: (data) => api.etl.run(data),
onSuccess: () => {
queryClient.invalidateQueries(['etl-jobs'])
alert('Pipeline ETL démarré avec succès!')
},
onError: (error) => {
alert(`Erreur: ${error.response?.data?.detail || error.message}`)
}
})
const handleSubmit = (e) => {
e.preventDefault()
runMutation.mutate(formData)
}
const handleChange = (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value
setFormData({ ...formData, [e.target.name]: value })
}
return (
<div>
<div className="page-header">
<h1>
Gestionnaire ETL
<HelpIcon text="ETL signifie Extract-Transform-Load (Extraire-Transformer-Charger). Ce processus extrait les données brutes du staging, les transforme au format OMOP CDM, et les charge dans les tables OMOP finales." />
</h1>
<p>Lancer et gérer les pipelines ETL</p>
</div>
<div className="card">
<h2>
Nouveau Pipeline ETL
<HelpIcon text="Configurez et lancez un nouveau pipeline ETL pour transformer vos données brutes en format OMOP CDM standardisé." />
</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>
Table source
<HelpIcon text="Table de staging contenant les données brutes à traiter. Les données doivent avoir le statut 'pending' pour être traitées." />
</label>
<select name="source_table" value={formData.source_table} onChange={handleChange}>
<option value="staging.raw_patients">staging.raw_patients</option>
<option value="staging.raw_visits">staging.raw_visits</option>
<option value="staging.raw_conditions">staging.raw_conditions</option>
<option value="staging.raw_drugs">staging.raw_drugs</option>
</select>
</div>
<div className="form-group">
<label>
Table cible
<HelpIcon text="Table OMOP CDM de destination où les données transformées seront chargées. Doit correspondre au type de données source." />
</label>
<select name="target_table" value={formData.target_table} onChange={handleChange}>
<option value="person">person</option>
<option value="visit_occurrence">visit_occurrence</option>
<option value="condition_occurrence">condition_occurrence</option>
<option value="drug_exposure">drug_exposure</option>
</select>
</div>
<div className="form-group">
<label>
Taille de batch
<HelpIcon text="Nombre d'enregistrements traités par lot. Des valeurs plus élevées (1000-5000) améliorent les performances mais consomment plus de mémoire." />
</label>
<input
type="number"
name="batch_size"
value={formData.batch_size}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label>
Nombre de workers
<HelpIcon text="Nombre de processus parallèles pour le traitement. Recommandé: 4-8 workers. Plus de workers = traitement plus rapide mais plus de charge CPU." />
</label>
<input
type="number"
name="num_workers"
value={formData.num_workers}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="sequential"
checked={formData.sequential}
onChange={handleChange}
/>
{' '}Mode séquentiel (pas de parallélisation)
<HelpIcon text="Active le traitement séquentiel (un enregistrement à la fois). Plus lent mais utile pour le débogage ou les petits volumes de données." />
</label>
</div>
<button type="submit" className="btn btn-primary" disabled={runMutation.isPending}>
{runMutation.isPending ? 'Démarrage...' : '🚀 Lancer le pipeline'}
</button>
</form>
</div>
<div className="card">
<h2>
Jobs en cours
<HelpIcon text="Liste des pipelines ETL actuellement en cours d'exécution avec leur progression en temps réel. Rafraîchissement automatique toutes les 2 secondes." />
</h2>
{Object.keys(jobs || {}).length === 0 ? (
<p>Aucun job en cours</p>
) : (
<table className="table">
<thead>
<tr>
<th>Job ID</th>
<th>Statut</th>
<th>Progression</th>
<th>Détails</th>
</tr>
</thead>
<tbody>
{Object.entries(jobs || {}).map(([jobId, job]) => (
<tr key={jobId}>
<td>{jobId}</td>
<td>
<span className={`badge badge-${job.status === 'completed' ? 'success' : job.status === 'failed' ? 'error' : 'warning'}`}>
{job.status}
</span>
</td>
<td>{job.progress || 0}%</td>
<td>
{job.stats && (
<span>
{job.stats.records_processed} enregistrements traités
</span>
)}
{job.error && <span className="error-message">{job.error}</span>}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)
}
export default ETLManager

View File

@@ -0,0 +1,116 @@
import React, { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { api } from '../api/client'
import HelpIcon from '../components/HelpIcon'
function Logs() {
const [lines, setLines] = useState(100)
const [level, setLevel] = useState('')
const { data: logs } = useQuery({
queryKey: ['logs', lines, level],
queryFn: () => api.logs.get(lines, level).then(res => res.data),
refetchInterval: 3000
})
const { data: errors } = useQuery({
queryKey: ['error-logs'],
queryFn: () => api.logs.errors(50).then(res => res.data)
})
return (
<div>
<div className="page-header">
<h1>
Logs système
<HelpIcon text="Consultez les logs d'application et les erreurs de validation. Utile pour diagnostiquer les problèmes et suivre l'activité du système." />
</h1>
<p>Consulter les logs et erreurs</p>
</div>
<div className="card">
<h2>
Filtres
<HelpIcon text="Filtrez les logs par nombre de lignes et niveau de sévérité (INFO, WARNING, ERROR, CRITICAL). Les logs se rafraîchissent automatiquement toutes les 3 secondes." />
</h2>
<div style={{ display: 'flex', gap: '15px', marginBottom: '20px' }}>
<div className="form-group" style={{ marginBottom: 0 }}>
<label>Nombre de lignes</label>
<select value={lines} onChange={(e) => setLines(Number(e.target.value))}>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
<option value={500}>500</option>
</select>
</div>
<div className="form-group" style={{ marginBottom: 0 }}>
<label>Niveau</label>
<select value={level} onChange={(e) => setLevel(e.target.value)}>
<option value="">Tous</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="CRITICAL">CRITICAL</option>
</select>
</div>
</div>
</div>
<div className="card">
<h2>
Logs récents
<HelpIcon text="Affichage en temps réel des logs d'application. Les messages incluent l'horodatage, le niveau de sévérité et les détails de l'événement." />
</h2>
<div style={{
background: '#1e1e1e',
color: '#d4d4d4',
padding: '15px',
borderRadius: '5px',
fontFamily: 'monospace',
fontSize: '12px',
maxHeight: '400px',
overflow: 'auto'
}}>
{logs?.logs?.map((line, idx) => (
<div key={idx}>{line}</div>
))}
</div>
</div>
<div className="card">
<h2>
Erreurs de validation
<HelpIcon text="Erreurs détectées lors de la validation des données OMOP. Chaque erreur indique la table, l'enregistrement concerné et le type de problème rencontré." />
</h2>
{errors?.errors?.length === 0 ? (
<p>Aucune erreur trouvée</p>
) : (
<table className="table">
<thead>
<tr>
<th>Table</th>
<th>Record ID</th>
<th>Type</th>
<th>Message</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{errors?.errors?.map((error) => (
<tr key={error.error_id}>
<td>{error.table_name}</td>
<td>{error.record_id}</td>
<td><span className="badge badge-error">{error.error_type}</span></td>
<td>{error.error_message}</td>
<td>{new Date(error.error_time).toLocaleString('fr-FR')}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)
}
export default Logs

View File

@@ -0,0 +1,111 @@
import React from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { api } from '../api/client'
import HelpIcon from '../components/HelpIcon'
function SchemaManager() {
const queryClient = useQueryClient()
const { data: schemaInfo } = useQuery({
queryKey: ['schema-info'],
queryFn: () => api.schema.info().then(res => res.data)
})
const { data: validation } = useQuery({
queryKey: ['schema-validation'],
queryFn: () => api.schema.validate().then(res => res.data)
})
const createMutation = useMutation({
mutationFn: (schemaType) => api.schema.create(schemaType),
onSuccess: () => {
queryClient.invalidateQueries(['schema-info'])
alert('Schéma créé avec succès!')
},
onError: (error) => {
alert(`Erreur: ${error.response?.data?.detail || error.message}`)
}
})
return (
<div>
<div className="page-header">
<h1>
Gestion des Schémas
<HelpIcon text="Gérez les schémas de base de données PostgreSQL. Le schéma OMOP contient les tables standardisées, Staging les données brutes, et Audit les logs d'exécution." />
</h1>
<p>Créer et valider les schémas de base de données</p>
</div>
<div className="card">
<h2>
Créer les schémas
<HelpIcon text="Créez les schémas et tables nécessaires dans PostgreSQL. Utilisez 'Créer tous les schémas' pour une installation complète ou créez-les individuellement." />
</h2>
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<button
className="btn btn-primary"
onClick={() => createMutation.mutate('all')}
disabled={createMutation.isPending}
>
Créer tous les schémas
</button>
<button
className="btn btn-success"
onClick={() => createMutation.mutate('omop')}
disabled={createMutation.isPending}
>
Schéma OMOP
</button>
<button
className="btn btn-success"
onClick={() => createMutation.mutate('staging')}
disabled={createMutation.isPending}
>
Schéma Staging
</button>
<button
className="btn btn-success"
onClick={() => createMutation.mutate('audit')}
disabled={createMutation.isPending}
>
Schéma Audit
</button>
</div>
</div>
<div className="card">
<h2>
État des schémas
<HelpIcon text="Validation automatique des schémas. Vérifie que toutes les tables requises existent et sont correctement structurées selon OMOP CDM 5.4." />
</h2>
{validation && (
<div className={validation.valid ? 'badge-success' : 'badge-error'} style={{ padding: '15px', borderRadius: '5px', marginBottom: '20px' }}>
{validation.message}
</div>
)}
{schemaInfo?.schemas && (
<table className="table">
<thead>
<tr>
<th>Schéma</th>
<th>Nombre de tables</th>
</tr>
</thead>
<tbody>
{Object.entries(schemaInfo.schemas).map(([schema, count]) => (
<tr key={schema}>
<td><strong>{schema}</strong></td>
<td>{count}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)
}
export default SchemaManager

View File

@@ -0,0 +1,82 @@
import React from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../api/client'
import HelpIcon from '../components/HelpIcon'
function Validation() {
const queryClient = useQueryClient()
const { data: unmappedCodes } = useQuery({
queryKey: ['unmapped-codes'],
queryFn: () => api.validation.unmappedCodes(50).then(res => res.data)
})
const runValidation = useMutation({
mutationFn: () => api.validation.run(),
onSuccess: () => {
alert('Validation lancée avec succès!')
queryClient.invalidateQueries(['unmapped-codes'])
}
})
return (
<div>
<div className="page-header">
<h1>
Validation des données
<HelpIcon text="Vérifiez la qualité et la conformité de vos données OMOP. Identifiez les codes non mappés, les valeurs manquantes et les problèmes de cohérence." />
</h1>
<p>Vérifier la qualité et la conformité OMOP</p>
</div>
<div className="card">
<h2>
Actions
<HelpIcon text="Lancez une validation complète des données OMOP. Le processus vérifie l'intégrité référentielle, les valeurs obligatoires et la conformité aux vocabulaires." />
</h2>
<button
className="btn btn-primary"
onClick={() => runValidation.mutate()}
disabled={runValidation.isPending}
>
{runValidation.isPending ? 'Validation en cours...' : '✅ Lancer la validation'}
</button>
</div>
<div className="card">
<h2>
Codes non mappés
<HelpIcon text="Liste des codes sources qui n'ont pas pu être mappés vers les vocabulaires OMOP standard. Ces codes nécessitent une attention pour améliorer la qualité des données." />
</h2>
{unmappedCodes?.unmapped_codes?.length === 0 ? (
<p>Aucun code non mappé trouvé</p>
) : (
<table className="table">
<thead>
<tr>
<th>Vocabulaire</th>
<th>Code</th>
<th>Nom</th>
<th>Fréquence</th>
<th>Dernière occurrence</th>
</tr>
</thead>
<tbody>
{unmappedCodes?.unmapped_codes?.map((code, idx) => (
<tr key={idx}>
<td>{code.source_vocabulary}</td>
<td><code>{code.source_code}</code></td>
<td>{code.source_name}</td>
<td><span className="badge badge-warning">{code.frequency}</span></td>
<td>{new Date(code.last_seen).toLocaleString('fr-FR')}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
)
}
export default Validation