diff --git a/README.md b/README.md index b5657cf..319b49f 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ Esquema completo: [`docs/database.dbml`](docs/database.dbml) ## Documentación por servicio +- [`docs/MIGRACION_MICROSERVICIOS.md`](docs/MIGRACION_MICROSERVICIOS.md) — migración desde `main`, arquitectura, escalabilidad y app desktop v1.0.0 - [`services/api_gateway/README.md`](services/api_gateway/README.md) - [`services/extraction/README.md`](services/extraction/README.md) - [`services/cleaning/README.md`](services/cleaning/README.md) diff --git a/docs/MIGRACION_MICROSERVICIOS.md b/docs/MIGRACION_MICROSERVICIOS.md new file mode 100644 index 0000000..0483ac7 --- /dev/null +++ b/docs/MIGRACION_MICROSERVICIOS.md @@ -0,0 +1,428 @@ +# Migración a microservicios y arquitectura IPSExtractor + +Documento de referencia para la rama `migration-services` respecto a `main`. +Describe qué hay de nuevo, cómo funciona la infraestructura, los patrones de diseño, cómo escalar el sistema y cómo incorporar servicios futuros. + +> **Nota:** el repositorio no tiene rama `master`; la línea base de comparación es **`main`**. + +--- + +## 1. Resumen ejecutivo + +IPSExtractor evolucionó de un **monolito Python** (interfaz gráfica de escritorio + API embebida) a una **arquitectura de microservicios** orquestada con Docker Compose, con una **aplicación de escritorio Electron + React** como cliente principal. + +| Aspecto | Estado actual | +|---------|---------------| +| Despliegue | `docker compose up` — stack completo en contenedores | +| Puerta de entrada | **API Gateway** en `0.0.0.0:8189` (único puerto expuesto al host) | +| Persistencia | PostgreSQL 16 + migraciones Alembic | +| Cola async | Redis 7 + workers ARQ para extracción LLM | +| Conteo | `count-api` (Go, Tesseract + ONNX), stateless | +| Cliente | IPSExtractor Desktop **v1.0.0** (2026-07-06), instalador NSIS Windows | + +**Alcance de la migración (rama `migration-services`):** ~170 archivos modificados, +27 000 / −8 500 líneas, 5 commits sobre `main`. + +--- + +## 2. Qué cambió respecto a `main` + +### Antes y después + +| Antes (`main`) | Ahora (`migration-services`) | +|----------------|------------------------------| +| `gui/` + `gui_main.py` — UI desktop en Python | `services/desktop_app/` — Electron + React + TypeScript | +| `server/app/` — API monolítica | `api_gateway` + `extraction` + `cleaning` + `count-api` | +| `application/` — lógica de dominio acoplada al monolito | Dominio repartido bajo `services//` | +| `installer.iss` / `build_installer.bat` — Inno Setup | `electron-builder` → `IPSExtractor-Desktop-1.0.0-Setup.exe` | +| Sin orquestación unificada | [`docker-compose.yml`](../docker-compose.yml) — stack completo | +| Sin modelo de trabajos en BD | PostgreSQL: `jobs`, `tasks`, conteo, extracción, limpieza | +| Procesamiento síncrono en el mismo proceso | Redis + workers ARQ (`WORKER_REPLICAS`) | + +### Eliminado + +- `gui/` — pestañas de extracción, limpieza, métricas, preclasificación, etc. +- `application/` en la raíz del repo +- `server/` — API FastAPI monolítica +- `gui_main.py`, `requirements.txt` raíz, `installer.iss` +- Tests legacy en `tests/` (sustituidos por tests por servicio / desktop) + +### Añadido (highlights) + +| Ruta | Rol | +|------|-----| +| `services/api_gateway/` | Proxy HTTP único hacia el resto de servicios | +| `services/extraction/` | Jobs, tasks, conteo en BD, extracción LLM, API REST | +| `services/extraction/worker/` | Workers ARQ para procesamiento asíncrono | +| `services/cleaning/` | Limpieza y transformación de datos CSV | +| `services/services-count-facturas-medicas-serving/` | `count-api` — OCR + clasificación ONNX (Go) | +| `services/db/` | Migraciones Alembic (PostgreSQL) | +| `services/desktop_app/` | App Electron + React | +| `docs/database.dbml` | Esquema completo de la base de datos | +| `.env.example` | Variables de entorno de referencia | + +### Commits en `migration-services` (sobre `main`) + +1. Eliminación del monolito legacy + introducción de Docker Compose microservicios +2. App desktop Electron + wizards de extracción +3. Redis, `.env.example`, indexación de carpetas +4. Servicio `db-migrations`, jobs y conteo en PostgreSQL +5. Healthchecks, puerto 8189, OCR tuning, instalador desktop v1.0.0 + +--- + +## 3. Infraestructura nueva + +### 3.1 Despliegue Docker + +Solo el **API Gateway** publica un puerto al host. El resto de servicios se comunican por la red interna de Docker. + +| Servicio | Puerto host | Puerto interno | Tecnología | +|----------|-------------|----------------|------------| +| `api_gateway` | **8189** | 8000 | Python / FastAPI | +| `extraction` | — | 8001 | Python / FastAPI | +| `cleaning` | — | 8002 | Python / FastAPI | +| `count-api` | — | 8080 | Go | +| `database` | — | 5432 | PostgreSQL 16 | +| `redis` | — | 6379 | Redis 7 | +| `worker` | — | — | Python ARQ (N réplicas) | +| `db-migrations` | — | — | Alembic (one-shot) | + +```mermaid +flowchart TB + subgraph host [Host] + Desktop[DesktopApp Electron] + Port8189["0.0.0.0:8189"] + end + subgraph docker [Red Docker interna] + GW[api_gateway :8000] + EXT[extraction :8001] + CLN[cleaning :8002] + CNT[count-api :8080] + WRK[worker ARQ x N] + PG[(PostgreSQL)] + RD[(Redis)] + end + Desktop -->|"HTTPS prod o 127.0.0.1:8189 dev"| Port8189 + Port8189 --> GW + GW --> EXT + GW --> CLN + GW --> CNT + EXT --> PG + EXT --> RD + WRK --> PG + WRK --> RD + CLN --> PG +``` + +### 3.2 Arranque y dependencias + +El orden de arranque está controlado por healthchecks y `depends_on`: + +1. `database` y `redis` deben estar **healthy** +2. `db-migrations` ejecuta Alembic y termina con éxito +3. `extraction`, `cleaning`, `count-api` y `worker` arrancan +4. `api_gateway` espera a `extraction` healthy y `count-api` healthy + +```bash +cp .env.example .env +docker compose up --build +curl http://127.0.0.1:8189/health/live +``` + +> En Windows use `127.0.0.1` en lugar de `localhost` para evitar timeouts por resolución IPv6 (`::1`). + +### 3.3 Flujo de negocio (centrado en jobs) + +Un **job** agrupa el trabajo del usuario (carpeta de documentos). Dentro del job se ejecutan **tasks** por módulo (`counting`, `extraction`, `cleaning`). + +```mermaid +sequenceDiagram + participant App as DesktopApp + participant GW as ApiGateway + participant CNT as CountApi + participant EXT as Extraction + participant DB as PostgreSQL + + App->>GW: POST /count/v1/count + GW->>CNT: proxy OCR+ONNX + CNT-->>App: resultados por PDF + App->>GW: POST /count-jobs + GW->>EXT: persistir conteo + EXT->>DB: count_job_files + App->>GW: POST /tasks + ingest-many + GW->>EXT: encolar extracción + EXT->>DB: extraction_rows +``` + +### 3.4 Rutas del API Gateway + +El gateway en [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py) actúa como reverse proxy: + +| Prefijo público | Servicio upstream | +|-----------------|-------------------| +| `/jobs`, `/tasks`, `/count-jobs` | `extraction:8001` | +| `/extraction/*` | `extraction:8001` | +| `/cleaning/*` | `cleaning:8002` | +| `/count/*` | `count-api:8080` | +| `/health`, `/health/live`, `/config`, `/models` | Agregación / proxy | + +Timeouts diferenciados: peticiones JSON (~30 s) vs uploads OCR/conteo (hasta 600 s). + +--- + +## 4. Patrones de diseño + +| Patrón | Ubicación | Propósito | +|--------|-----------|-----------| +| **API Gateway / Reverse Proxy** | [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py) | Un solo endpoint público; enrutamiento por prefijo | +| **Microservicios sin paquetes compartidos** | `services/*` | Cada servicio con Dockerfile, dominio y dependencias propias | +| **Cola de trabajos async** | [`services/extraction/worker/`](../services/extraction/worker/), Redis | Extracción LLM fuera del ciclo request/response HTTP | +| **Registry / Plugin (desktop)** | [`services/desktop_app/src/renderer/modules/registry.ts`](../services/desktop_app/src/renderer/modules/registry.ts) | Módulos `counting` / `extraction` se registran sin acoplar `App.tsx` | +| **Job + Task (agregado)** | Tablas `jobs` → `tasks` en PostgreSQL | Un trabajo del usuario agrupa módulos ejecutados | +| **Healthchecks en cadena** | [`docker-compose.yml`](../docker-compose.yml) | `depends_on: service_healthy` evita arranques prematuros | +| **BFF con timeouts por ruta** | Gateway `_is_upload_path()` | JSON ligero vs uploads pesados | +| **Stateless vs stateful** | `count-api` (stateless) vs `extraction` (stateful + BD) | Conteo horizontalmente escalable; extracción con persistencia | + +### Registry de módulos (desktop) + +Cada módulo implementa `ModuleDescriptor` y se auto-registra al importarse: + +```typescript +// services/desktop_app/src/renderer/modules/counting/CountingModule.ts +registerModule({ + type: "counting", + label: "Conteo", + Wizard: CountingWizardModule, + Results: CountingResults, + hasTask: (tasks) => tasks.some((t) => t.type === "counting"), +}); +``` + +`App.tsx` solo importa los módulos; no necesita conocer su implementación interna. + +--- + +## 5. Componentes principales + +### 5.1 Backend + +| Servicio | Responsabilidad | +|----------|-----------------| +| **api_gateway** | Proxy HTTP, `/health/live`, CORS, logging, errores 503/504 hacia upstream caído | +| **extraction** | CRUD de jobs/tasks, count-jobs, ingest de PDFs, estimación de costos, API LLM | +| **worker** | Procesamiento ARQ en background; escala con `WORKER_REPLICAS` | +| **count-api** | OCR (Tesseract) + inferencia ONNX por lote; sin estado ni BD | +| **cleaning** | Reglas de limpieza CSV; expuesto vía `/cleaning/*` | +| **db-migrations** | Alembic one-shot antes de que arranquen los servicios de aplicación | + +### 5.2 Base de datos (resumen) + +``` +jobs — contenedor de un trabajo del usuario + tasks — módulo ejecutado (counting / extraction / cleaning) + count_job_files — resultado de conteo por PDF + extraction_batches — lote de extracción + extraction_rows — fila extraída por documento + cleaning_jobs — job de limpieza + cleaned_rows — fila transformada +``` + +Esquema completo: [`database.dbml`](database.dbml) + +### 5.3 Desktop (`services/desktop_app/`) + +| Componente | Función | +|------------|---------| +| `App.tsx` | Navegación Home / Job / Settings; sidebar con submenú por job | +| `HomeView.tsx` | Listado y creación de jobs | +| `JobView.tsx` | Paneles de conteo y extracción (progreso en segundo plano) | +| `JobResultsView.tsx` | Resultados unificados, export CSV, lanzar extracción desde válidos | +| `CountingWizard.tsx` | Lotes paralelos hacia `POST /count/v1/count` | +| `JobExtractionWizard.tsx` | Extracción con upload de PDFs desde el escritorio | +| `apiClient.ts` | Cliente HTTP único hacia el gateway (timeouts, mensajes de error) | +| `electron/folderIndex.cjs` | Indexación local: modo `general` o `radicados` | +| `electron/preload.cjs` | Bridge seguro main ↔ renderer (`contextBridge`) | + +--- + +## 6. Cómo escalar para mayor velocidad + +### 6.1 Conteo (`count-api`) + +El conteo es el cuello de botella más frecuente en lotes grandes de PDFs. + +| Variable / ajuste | Efecto | Dónde | +|-------------------|--------|-------| +| `OCR_DPI=150` | ~4× menos RAM que 300 DPI | `docker-compose.yml` | +| `OCR_POOL_SIZE` | Goroutines OCR por request | `docker-compose.yml` | +| `SYNC_PROCESS_CONCURRENCY` | Paralelismo interno por request | `docker-compose.yml` | +| `COUNT_API_MEMORY` | Límite RAM del contenedor (evitar OOM) | `docker-compose.yml` | +| `restart: unless-stopped` | Recuperación automática tras OOM | `docker-compose.yml` | +| `BATCH_SIZE=10` | PDFs por petición HTTP | `CountingWizard.tsx` | +| `BATCH_CONCURRENCY=2` | Lotes HTTP simultáneos desde desktop | `CountingWizard.tsx` | + +**Futuro:** réplicas de `count-api` detrás del gateway con balanceo round-robin en el proxy. + +### 6.2 Extracción LLM + +| Variable / comando | Efecto | +|--------------------|--------| +| `WORKER_REPLICAS=4` | Más workers ARQ procesando en paralelo | +| `docker compose up --scale worker=4` | Escala horizontal de workers | +| `WORKER_CONCURRENCY` | Tareas concurrentes por worker | +| `CONCURRENCY_LIMIT` | Límite global en el servicio extraction | +| `PDF_THREAD_COUNT` | Hilos para renderizado de PDF | +| `PDF_DPI` | Resolución de páginas enviadas al LLM | + +### 6.3 Infraestructura y build + +| Problema | Solución | +|----------|----------| +| BuildKit satura al construir `count-api` | `docker compose build count-api` antes del stack completo | +| Build paralelo falla en Docker Desktop | `COMPOSE_BAKE=false COMPOSE_PARALLEL_LIMIT=2` (ver `.env.example`) | +| Conexiones BD colgadas | Pool SQLAlchemy + `connect_timeout` en [`database.py`](../services/extraction/infrastructure/database.py) | +| Gateway sin respuesta en Windows | Usar `127.0.0.1:8189`, no `localhost` | + +--- + +## 7. Cómo añadir un servicio futuro + +Checklist para incorporar un nuevo microservicio sin romper la arquitectura: + +### Paso 1 — Crear el servicio + +``` +services/mi-servicio/ + Dockerfile + api/main.py # API propia (FastAPI, Go, etc.) + domain/ # lógica de negocio + requirements.txt + README.md +``` + +### Paso 2 — Registrar en Docker Compose + +En [`docker-compose.yml`](../docker-compose.yml): + +- Añadir el servicio **sin** `ports` al host (solo red interna) +- Definir `healthcheck` si expone `/health` +- Configurar `depends_on` según dependencias (BD, migraciones, etc.) + +### Paso 3 — Exponer vía API Gateway + +En [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py): + +```python +MI_SERVICIO_URL = os.getenv("MI_SERVICIO_URL", "http://mi-servicio:8003").rstrip("/") + +@app.api_route("/mi-servicio/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) +async def mi_servicio_proxy(path: str, request: Request) -> Response: + return await _proxy_request(request, f"{MI_SERVICIO_URL}/{path}") +``` + +Añadir `MI_SERVICIO_URL` en el bloque `environment` de `api_gateway` en compose. + +### Paso 4 — Base de datos (si aplica) + +1. Crear migración Alembic en `services/db/alembic/versions/` +2. El servicio `db-migrations` la aplicará en el próximo `docker compose up` + +### Paso 5 — Módulo en la app desktop (si aplica) + +1. Crear `services/desktop_app/src/renderer/modules/mi-modulo/` +2. Implementar `ModuleDescriptor` y llamar `registerModule()` +3. Importar el módulo en `App.tsx` (una línea, igual que counting/extraction) +4. Añadir entrada en el sidebar de `JobView` si requiere sección propia + +### Paso 6 — Documentación + +- README del servicio +- Actualizar tabla de servicios en [`README.md`](../README.md) +- Actualizar este documento si el patrón de integración cambia + +```mermaid +flowchart LR + NewSvc[nuevo_servicio] + Compose[docker-compose] + GW[api_gateway nueva ruta] + Reg[registerModule desktop] + NewSvc --> Compose + Compose --> GW + GW --> Desktop + Reg --> Desktop +``` + +--- + +## 8. App de escritorio — versión y funcionalidades + +### 8.1 Versión e instalador + +| Campo | Valor | +|-------|-------| +| **Versión** | 1.0.0 | +| **Fecha de release** | 2026-07-06 | +| **Instalador** | `services/desktop_app/release/IPSExtractor-Desktop-1.0.0-Setup.exe` | +| **Formato** | NSIS, Windows x64 | +| **Fuente de versión** | `services/desktop_app/package.json` + `src/shared/version.json` | + +Generar instalador: + +```bash +cd services/desktop_app +npm install +npm run dist +``` + +### 8.2 URL del API Gateway + +| Entorno | URL por defecto | +|---------|-----------------| +| Desarrollo (`npm run dev`) | `http://127.0.0.1:8189` | +| Instalador (build producción) | `https://pys1.builder.xyz` | + +La URL es configurable en **Configuración** de la app y se persiste en `localStorage`. + +### 8.3 Funcionalidades incluidas + +- **Gestión de trabajos (jobs):** crear, abrir y organizar trabajos con carpeta local de documentos +- **Conteo:** OCR + clasificación ML en lotes con barra de progreso en segundo plano +- **Validación humana:** marcar archivos del conteo como válidos/inválidos +- **Extracción LLM:** wizard con descubrimiento de campos, estimación de costo y ejecución +- **Extracción desde conteo:** lanzar extracción solo sobre PDFs válidos del conteo +- **Resultados unificados:** vista de conteo y extracción con exportación CSV +- **Configuración:** URL del gateway, verificación de conexión, vista de config/modelos del backend +- **Versión visible:** sidebar (menú) y esquina superior derecha (`v1.0.0 · 6 jul 2026`) + +### 8.4 Arquitectura de la UI + +``` +Home (lista de jobs) + └── Job activo + ├── Conteo → CountingWizardModule + ├── Extracción → JobExtractionWizard + └── Resultados → JobResultsView +Configuración (global) +``` + +La navegación usa sidebar lateral con submenú contextual por job (sin tabs en el área principal). + +--- + +## 9. Enlaces relacionados + +| Documento | Descripción | +|-----------|-------------| +| [`README.md`](../README.md) | Guía rápida del stack completo | +| [`services/api_gateway/README.md`](../services/api_gateway/README.md) | API Gateway | +| [`services/extraction/README.md`](../services/extraction/README.md) | Extracción LLM y jobs | +| [`services/cleaning/README.md`](../services/cleaning/README.md) | Limpieza de datos | +| [`services/services-count-facturas-medicas-serving/README.md`](../services/services-count-facturas-medicas-serving/README.md) | Count API (Go) | +| [`services/db/README.md`](../services/db/README.md) | Migraciones Alembic | +| [`services/desktop_app/README.md`](../services/desktop_app/README.md) | App Electron | +| [`database.dbml`](database.dbml) | Esquema completo de PostgreSQL | +| [`.env.example`](../.env.example) | Variables de entorno | + +--- + +*Última actualización: 2026-07-06 — rama `migration-services`.* diff --git a/services/desktop_app/electron/main.cjs b/services/desktop_app/electron/main.cjs index 0d3ea66..5f91f21 100644 --- a/services/desktop_app/electron/main.cjs +++ b/services/desktop_app/electron/main.cjs @@ -26,7 +26,7 @@ function createWindow() { win.loadURL("http://127.0.0.1:5173"); win.webContents.openDevTools({ mode: "detach" }); } else { - win.loadFile(path.join(__dirname, "../dist/index.html")); + win.loadFile(path.join(app.getAppPath(), "dist", "index.html")); } } diff --git a/services/desktop_app/vite.config.ts b/services/desktop_app/vite.config.ts index 55c8d6c..4903217 100644 --- a/services/desktop_app/vite.config.ts +++ b/services/desktop_app/vite.config.ts @@ -12,6 +12,8 @@ const DEVELOPMENT_API_BASE_URL = "http://127.0.0.1:8189"; export default defineConfig(({ mode }) => ({ plugins: [react()], + // Rutas relativas para que loadFile() funcione en el instalador (protocolo file://). + base: "./", define: { __APP_VERSION__: JSON.stringify(pkg.version), __APP_RELEASE_DATE__: JSON.stringify(versionMeta.releaseDate),