diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 0000000..d20c0fe --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/.gitignore b/.gitignore index 868f935..b8bf38f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ build/ develop-eggs/ dist/ downloads/ +!services/desktop_app/src/renderer/modules/downloads/ +!services/desktop_app/src/renderer/modules/downloads/** eggs/ .eggs/ lib/ diff --git a/README.md b/README.md index 12d70ff..8d2f66d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# IPSExtractor +# Flujo IPS Sistema de extracción y análisis de datos desde facturas médicas PDF. Arquitectura de microservicios: cada servicio tiene su propia API, dependencias, Dockerfile y código de dominio. No hay paquetes compartidos entre servicios. diff --git a/bots/bot_descarga/bot_descarga/ui/dashboard.py b/bots/bot_descarga/bot_descarga/ui/dashboard.py index 5502f8f..fb98fa3 100644 --- a/bots/bot_descarga/bot_descarga/ui/dashboard.py +++ b/bots/bot_descarga/bot_descarga/ui/dashboard.py @@ -68,7 +68,7 @@ class BotDashboard(ctk.CTk): ctk.CTkLabel( config, - text="API remota (IPSExtractor)", + text="API remota (Flujo IPS)", font=ctk.CTkFont(size=16, weight="bold"), ).pack(anchor="w", padx=18, pady=(14, 8)) diff --git a/bots/bot_digitador/bot_digitador/ui/dashboard.py b/bots/bot_digitador/bot_digitador/ui/dashboard.py index ca1b7bd..06dffc0 100644 --- a/bots/bot_digitador/bot_digitador/ui/dashboard.py +++ b/bots/bot_digitador/bot_digitador/ui/dashboard.py @@ -68,7 +68,7 @@ class BotDashboard(ctk.CTk): ctk.CTkLabel( config, - text="API remota (IPSExtractor)", + text="API remota (Flujo IPS)", font=ctk.CTkFont(size=16, weight="bold"), ).pack(anchor="w", padx=18, pady=(14, 8)) diff --git a/services/api_gateway/api/main.py b/services/api_gateway/api/main.py index 645e34c..184ef1f 100644 --- a/services/api_gateway/api/main.py +++ b/services/api_gateway/api/main.py @@ -50,7 +50,7 @@ async def lifespan(app: FastAPI): _http_client = None -app = FastAPI(title="IPSExtractor API Gateway", version="1.0.0", lifespan=lifespan) +app = FastAPI(title="Flujo IPS API Gateway", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, diff --git a/services/bot_descarga/infrastructure/store.py b/services/bot_descarga/infrastructure/store.py index 3991fd9..db27114 100644 --- a/services/bot_descarga/infrastructure/store.py +++ b/services/bot_descarga/infrastructure/store.py @@ -567,17 +567,18 @@ class JobStore: exitosos += int(r.get("exitosos") or 0) fallidos += int(r.get("fallidos") or 0) uploaded += int(r.get("uploaded_count") or 0) - # Progreso parcial mientras corre - if c.status == JobStatus.RUNNING.value: - total = max(total, int(r.get("total") or 0)) - exitosos = max(exitosos, int(r.get("processed") or r.get("exitosos") or 0)) # Agregar progreso en vuelo (processed) de shards running processed = 0 for c in children: r = dict(c.result or {}) if c.status in terminal: - processed += int(r.get("exitosos") or 0) + int(r.get("fallidos") or 0) + # El agent reemplaza el resultado parcial al finalizar, por eso + # el fallback se calcula con los contadores finales. + processed += int( + r.get("processed") + or (int(r.get("exitosos") or 0) + int(r.get("fallidos") or 0)) + ) else: processed += int(r.get("processed") or 0) diff --git a/services/bot_descarga/tests/test_store.py b/services/bot_descarga/tests/test_store.py index 0353ce7..e42da62 100644 --- a/services/bot_descarga/tests/test_store.py +++ b/services/bot_descarga/tests/test_store.py @@ -175,3 +175,48 @@ def test_list_jobs_by_parent(store): listed = job_store.list_jobs(limit=50, job_id="parent-1") assert len(listed) == 1 assert listed[0].id == a.id + + +def test_distributed_parent_reports_live_processed_total(store): + job_store, JobStatus, _WorkerStatus = store + parent = job_store.create_job( + None, + "uploads/parent/input.xlsx", + "input.xlsx", + shard_count=2, + result={"total_rows": 10, "processed": 0}, + status=JobStatus.PENDING.value, + ) + first = job_store.create_job( + "PC-01", + "uploads/first/input.xlsx", + parent_download_job_id=parent.id, + shard_index=0, + shard_count=2, + result={"total": 5, "processed": 2}, + status=JobStatus.RUNNING.value, + ) + second = job_store.create_job( + "PC-02", + "uploads/second/input.xlsx", + parent_download_job_id=parent.id, + shard_index=1, + shard_count=2, + result={"total": 5, "processed": 0}, + status=JobStatus.PENDING.value, + ) + + live = job_store.refresh_parent_from_shards(parent.id) + assert live is not None + assert live.result["processed"] == 2 + assert live.result["total"] == 10 + + job_store.patch_job( + first.id, + status=JobStatus.COMPLETED.value, + result={"total": 5, "exitosos": 3, "fallidos": 2}, + ) + job_store.patch_job(second.id, result={"total": 5, "processed": 1}) + live = job_store.refresh_parent_from_shards(parent.id) + assert live is not None + assert live.result["processed"] == 6 diff --git a/services/bot_digitador/infrastructure/store.py b/services/bot_digitador/infrastructure/store.py index 5894bd7..6ec2374 100644 --- a/services/bot_digitador/infrastructure/store.py +++ b/services/bot_digitador/infrastructure/store.py @@ -555,27 +555,37 @@ class JobStore: all_terminal = all(s in terminal for s in statuses) total = 0 - ok = 0 - error_count = 0 - timeout = 0 + exitosos = 0 + fallidos = 0 + omitidos = 0 + timeouts = 0 existentes = 0 + uploaded = 0 for c in children: r = dict(c.result or {}) total += int(r.get("total") or 0) - ok += int(r.get("ok") or 0) - error_count += int(r.get("error") or 0) - timeout += int(r.get("timeout") or 0) + # Los agents actuales publican los nombres en español. Se + # conservan los aliases anteriores para jobs históricos. + exitosos += int(r.get("exitosos") or r.get("ok") or 0) + fallidos += int(r.get("fallidos") or r.get("error") or 0) + omitidos += int(r.get("omitidos") or 0) + timeouts += int(r.get("timeouts") or r.get("timeout") or 0) existentes += int(r.get("existentes") or 0) + uploaded += int(r.get("uploaded_count") or 0) processed = 0 for c in children: r = dict(c.result or {}) if c.status in terminal: - processed += ( - int(r.get("ok") or 0) - + int(r.get("error") or 0) - + int(r.get("timeout") or 0) - + int(r.get("existentes") or 0) + # fallidos ya incluye omitidos + timeouts; no se suman de + # nuevo para evitar que el progreso supere el total. + processed += int( + r.get("processed") + or ( + int(r.get("exitosos") or r.get("ok") or 0) + + int(r.get("fallidos") or r.get("error") or 0) + + int(r.get("existentes") or 0) + ) ) else: processed += int(r.get("processed") or 0) @@ -588,11 +598,13 @@ class JobStore: "shard_ids": shard_ids, "total": total or int(parent_result.get("total_rows") or 0), "total_rows": int(parent_result.get("total_rows") or total or 0), - "ok": ok, - "error": error_count, - "timeout": timeout, + "exitosos": exitosos, + "fallidos": fallidos, + "omitidos": omitidos, + "timeouts": timeouts, "existentes": existentes, "processed": processed, + "uploaded_count": uploaded, "reports_prefix": parent.reports_prefix, "shards": [ { diff --git a/services/bot_digitador/tests/test_store.py b/services/bot_digitador/tests/test_store.py new file mode 100644 index 0000000..210d462 --- /dev/null +++ b/services/bot_digitador/tests/test_store.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import importlib +import sys + +import pytest + + +@pytest.fixture() +def store(tmp_path, monkeypatch): + db_url = f"sqlite:///{(tmp_path / 'digitador.db').as_posix()}" + monkeypatch.setenv("DATABASE_URL", db_url) + for name in ["infrastructure.database", "infrastructure.store"]: + if name in sys.modules: + importlib.reload(sys.modules[name]) + else: + importlib.import_module(name) + + from infrastructure import database as db + + importlib.reload(db) + import infrastructure.store as store_mod + + importlib.reload(store_mod) + from domain.models import JobStatus + + return store_mod.JobStore(), JobStatus + + +def test_distributed_parent_uses_agent_counters_and_live_progress(store): + job_store, JobStatus = store + parent = job_store.create_job( + None, + "uploads/parent/input.csv", + "input.csv", + shard_count=2, + result={"total_rows": 8, "processed": 0}, + status=JobStatus.PENDING.value, + ) + first = job_store.create_job( + "PC-01", + "uploads/first/input.csv", + parent_digitador_job_id=parent.id, + shard_index=0, + shard_count=2, + result={"total": 4, "processed": 2}, + status=JobStatus.RUNNING.value, + ) + second = job_store.create_job( + "PC-02", + "uploads/second/input.csv", + parent_digitador_job_id=parent.id, + shard_index=1, + shard_count=2, + result={"total": 4, "processed": 1}, + status=JobStatus.RUNNING.value, + ) + + live = job_store.refresh_parent_from_shards(parent.id) + assert live is not None + assert live.result["processed"] == 3 + assert live.result["total"] == 8 + + job_store.patch_job( + first.id, + status=JobStatus.COMPLETED.value, + result={ + "total": 4, + "exitosos": 1, + "fallidos": 2, + "omitidos": 1, + "timeouts": 1, + "existentes": 1, + "uploaded_count": 3, + }, + ) + live = job_store.refresh_parent_from_shards(parent.id) + assert live is not None + assert live.result["processed"] == 5 + assert live.result["exitosos"] == 1 + assert live.result["fallidos"] == 2 + assert live.result["existentes"] == 1 + assert live.result["timeouts"] == 1 + assert live.result["uploaded_count"] == 3 diff --git a/services/cleaning/api/main.py b/services/cleaning/api/main.py index 1e1c3f8..b2dced1 100644 --- a/services/cleaning/api/main.py +++ b/services/cleaning/api/main.py @@ -40,7 +40,7 @@ async def lifespan(app: FastAPI): app = FastAPI( - title="IPSExtractor Cleaning Service", + title="Flujo IPS Cleaning Service", version="1.0.0", lifespan=lifespan, ) diff --git a/services/desktop_app/README.md b/services/desktop_app/README.md index 458f6f1..2a522fb 100644 --- a/services/desktop_app/README.md +++ b/services/desktop_app/README.md @@ -1,6 +1,6 @@ # desktop_app -Aplicación de escritorio Electron + React para interactuar con el stack IPSExtractor. +Aplicación de escritorio Electron + React de **Flujo IPS** para interactuar con el stack de servicios. Permite seleccionar carpetas locales, contar servicios en facturas médicas, revisar resultados y lanzar extracciones LLM. --- @@ -147,7 +147,7 @@ Layout en el bucket `apps-versions`: IPSExtractor/ latest.json versions.json ← catálogo de todas las versiones publicadas - 1.0.1/IPSExtractor-Desktop-1.0.1-Setup.exe + 1.0.1/Flujo-IPS-1.0.1-Setup.exe 1.0.1/DATA.txt ← fecha DD/MM/YYYY, ej. 06/07/2026 ``` diff --git a/services/desktop_app/electron/autoUpdate.cjs b/services/desktop_app/electron/autoUpdate.cjs index 3d0bceb..19a6341 100644 --- a/services/desktop_app/electron/autoUpdate.cjs +++ b/services/desktop_app/electron/autoUpdate.cjs @@ -64,7 +64,7 @@ function download(urlStr, opts = {}) { url, { timeout: FETCH_TIMEOUT_MS, - headers: { "User-Agent": `IPSExtractor-Desktop/${CURRENT_VERSION}` }, + headers: { "User-Agent": `Flujo-IPS/${CURRENT_VERSION}` }, }, (res) => { if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { diff --git a/services/desktop_app/electron/main.cjs b/services/desktop_app/electron/main.cjs index b0ee697..f6f452d 100644 --- a/services/desktop_app/electron/main.cjs +++ b/services/desktop_app/electron/main.cjs @@ -18,7 +18,7 @@ function createWindow() { height: 860, minWidth: 1080, minHeight: 720, - title: `IPSExtractor Desktop v${appVersion}`, + title: `Flujo IPS v${appVersion}`, icon: APP_ICON, backgroundColor: "#f6f7f9", webPreferences: { diff --git a/services/desktop_app/index.html b/services/desktop_app/index.html index c8bb5c3..a9e3c19 100644 --- a/services/desktop_app/index.html +++ b/services/desktop_app/index.html @@ -4,7 +4,7 @@ - IPSExtractor Desktop + Flujo IPS
diff --git a/services/desktop_app/package.json b/services/desktop_app/package.json index 1a8e877..86d5b96 100644 --- a/services/desktop_app/package.json +++ b/services/desktop_app/package.json @@ -2,7 +2,7 @@ "name": "ipsextractor-desktop", "version": "1.3.0", "private": true, - "description": "Desktop extraction client for IPSExtractor microservices.", + "description": "Aplicación de escritorio Flujo IPS para automatizar procesos de cuentas médicas.", "main": "electron/main.cjs", "scripts": { "dev": "concurrently -k \"vite --host 127.0.0.1\" \"wait-on tcp:5173 && electron .\"", @@ -41,8 +41,8 @@ }, "build": { "appId": "com.ipsextractor.desktop", - "productName": "IPSExtractor Desktop", - "artifactName": "IPSExtractor-Desktop-${version}-Setup.${ext}", + "productName": "Flujo IPS", + "artifactName": "Flujo-IPS-${version}-Setup.${ext}", "directories": { "output": "release", "buildResources": "resources" @@ -75,8 +75,8 @@ "en-US" ], "language": "1042", - "shortcutName": "IPSExtractor Desktop", - "uninstallDisplayName": "IPSExtractor Desktop ${version}" + "shortcutName": "Flujo IPS", + "uninstallDisplayName": "Flujo IPS ${version}" } } } diff --git a/services/desktop_app/scripts/publish-version.mjs b/services/desktop_app/scripts/publish-version.mjs index e14fc44..70ced88 100644 --- a/services/desktop_app/scripts/publish-version.mjs +++ b/services/desktop_app/scripts/publish-version.mjs @@ -61,7 +61,7 @@ if (!accessKey || !secretKey) { process.exit(1); } -const setupName = `IPSExtractor-Desktop-${version}-Setup.exe`; +const setupName = `Flujo-IPS-${version}-Setup.exe`; const releaseDir = getReleaseDir(); const setupPath = join(releaseDir, setupName); if (!existsSync(setupPath)) { diff --git a/services/desktop_app/src/renderer/App.test.tsx b/services/desktop_app/src/renderer/App.test.tsx index ec19b21..77efe87 100644 --- a/services/desktop_app/src/renderer/App.test.tsx +++ b/services/desktop_app/src/renderer/App.test.tsx @@ -10,7 +10,7 @@ describe("App", () => { text: async () => "[]", } as Response); render(); - expect(screen.getByText("IPSExtractor")).toBeInTheDocument(); + expect(screen.getByText("Flujo IPS")).toBeInTheDocument(); expect(screen.getByRole("button", { name: /^trabajos$/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /configuraci[oó]n/i })).toBeInTheDocument(); }); diff --git a/services/desktop_app/src/renderer/App.tsx b/services/desktop_app/src/renderer/App.tsx index 4f09ca2..5af0624 100644 --- a/services/desktop_app/src/renderer/App.tsx +++ b/services/desktop_app/src/renderer/App.tsx @@ -277,9 +277,9 @@ function App() {