From 69bcd297f87804304c260402a6449fcaebea7c20 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 16:30:35 +0000 Subject: [PATCH] docs: contexto del sistema PANELS para agentes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade un documento copiable con arquitectura, contratos RPC, módulos de negocio, rutas y reglas de no-romper según el código actual. Co-authored-by: alberto.martinez --- docs/contexto-sistema.md | 264 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 docs/contexto-sistema.md diff --git a/docs/contexto-sistema.md b/docs/contexto-sistema.md new file mode 100644 index 0000000..3e71ede --- /dev/null +++ b/docs/contexto-sistema.md @@ -0,0 +1,264 @@ +# PANELS — contexto del sistema (estado actual) + +Documento para pegar en reglas / contexto de un agente. Describe el producto **como está en el código hoy**, no un roadmap. + +Producto: **PANELS** (repo `panels`, paquete raíz `panel-obra`). Escritorio de obra para constructoras (México): padrón, obras, presupuesto, programa de obra, costos, almacén, gafetes, nómina. Multi-tenant. Consola SaaS aparte para operadores de plataforma. + +--- + +## 1. Mapa de repos / carpetas + +| Ruta | Qué es | +|------|--------| +| `api/` | API Deno 2 + Hono. Punto de entrada `api/main.ts`. Puerto **8000**. Prefijo `/v1`. | +| `web-panel/` | Escritorio de obra. Nuxt 3 SPA (`ssr: false`), PrimeVue 4, puerto **3000**. | +| `web-saas/` | Consola operadores. Nuxt 3 SPA, mismo stack, puerto **3001**. | +| `web/` | Reservada para website pública. **No** es el producto. | +| `db/` | Liquibase: `db/platform`, `db/iam`, `db/core`. Provision: `db/provision/`. | +| `docs/` | Ops (`coolify.md`), padrón (`padron-v2.md`), este contexto. | +| `overlays/compose.local.yml` | Solo local: Postgres + Redis + provision. **No** usar en Coolify. | +| `docker-compose.yml` | Producción: `migrate` → `api` → `web-panel` + `web-saas`. | + +Fronts (dev y nginx) proxifican `/v1` a la API. Cookies same-origin. En prod la API **no** lleva FQDN propio. + +--- + +## 2. Stack y runtime + +- **API:** Deno 2.9, Hono 4, `xlsx`, `pdf-lib`, `qrcode`. Sin FFI. Sin Java en el contenedor de tráfico. +- **Migraciones:** Liquibase + JRE 21 en imagen `migrate` (un shot por deploy). **No** corren al arrancar la API. +- **Fronts:** Nuxt 3.16, Vue 3, PrimeVue 4, PrimeIcons, Chart.js (panel). Build → nginx Alpine. +- **Postgres 16** + **Redis 7**. Object storage S3-compatible (**Cloudflare R2**). Fallback disco `/app/data` solo local. +- **CI:** GitHub Actions — provision + Liquibase + aislamiento + `deno check`/`deno test` + build de ambos fronts. + +Dev local: + +```bash +npm run dev:api # api +npm run dev:web # web-panel :3000 +npm run dev:saas # web-saas :3001 +# Docker todo-en-uno: +docker compose -f docker-compose.yml -f overlays/compose.local.yml up --build +``` + +--- + +## 3. Arquitectura de datos (monolito modular) + +Una instancia Postgres **por ambiente**, **dos bases**. Sin FK entre bases ni entre esquemas `iam` y `core`. `tenant_id` es referencia **lógica** validada en aplicación. + +| Base | Esquema | Changelog | Contenido | +|------|---------|-----------|-----------| +| `panels_platform` | `public` | `db/platform/` | Control plane: `tenants`, `platform_users`, `smtp_settings`. Solo rutas `/v1/saas/*`. | +| `panels_product` | `iam` | `db/iam/` | Usuarios/roles/permisos **del tenant** (quien entra a `web-panel`). | +| `panels_product` | `core` | `db/core/` | Negocio: empresas, obras, personal, docs, presupuesto, nómina, gafetes, gastos, almacén, programa de obra. | + +**Dos identidades distintas:** + +- `platform_users` = operadores PANELS (`web-saas`). Rol conceptual `platform_admin`. +- `iam.users` = usuarios de obra. Roles `tenant_admin` y `user`. + +Roles Postgres: `panels_{platform,iam,core}_{owner,app}`. Runtime usa `_app`. Liquibase/bootstrap usan `_owner`. Login por username usa `DATABASE_URL_IAM_OWNER` a propósito (bypassa RLS). + +Redis ACL: `panels_iam_redis` (sesiones) y `panels_core_redis` (cache). + +RLS en `core` e `iam`: el middleware `withCoreScope` (`api/scope.ts`) fija `app.tenant_id` en la transacción. Sin eso, políticas fail-closed devuelven 0 filas. + +Liquibase: **siempre** pasar `--context-filter`. `context="dev"` = demo (tenant ARCTEC `ARCT2608`). Producción: `--context-filter=!dev`. Sin filtro, Liquibase corre **todo**, incluido demo. + +--- + +## 4. Contrato RPC (capa de negocio) + +Lógica de `core` (y parte de `iam`) vive en funciones Postgres `core.fn_*` / `iam.fn_*`. La API **no** debe `db.prepare()` contra tablas de negocio en rutas nuevas; solo `api/rpc.ts` (`callCoreFn` → `SELECT fn($1::jsonb)`). + +Envelope JSON de toda función: + +```json +{ + "ok": true, + "code": "OK", + "layer": "db", + "message": "Mensaje específico en español", + "context": { "fn": "fn_..." }, + "data": {}, + "errors": null +} +``` + +Códigos: `OK`, `CREATED`, `VALIDATION`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `INTERNAL`, `NETWORK`. +`api/http_errors.ts` mapea a HTTP y añade `status`. `layer` puede ser `db` | `api` | `front`. Mensajes genéricos están prohibidos. + +Convenciones SQL: un parámetro `payload jsonb`, `SECURITY INVOKER`, `SET search_path = core` (o `iam`), `GRANT EXECUTE` al rol `_app` en el mismo changeset. Changesets core RPC: `006` (infra) … `030` (programa de obra). + +--- + +## 5. Auth, sesión, permisos + +- Login `POST /v1/auth/login`. Sesión en Redis IAM, cookie. `COOKIE_SECURE` en HTTPS. +- `GET /v1/auth/me`, `POST /v1/auth/logout`, `POST /v1/auth/change-password`. +- Alternativa: header `X-API-Key` **+** `X-Tenant-Id` obligatorio (no ve todos los tenants). +- `web-panel` rechaza `realm === "platform"` y manda al SaaS. +- Primer admin: `api/scripts/bootstrap-admin.ts` + `SEED_PASSWORD` (no seed de hash en Liquibase). + +Permisos (`iam.permissions`): + +| Código | Uso | +|--------|-----| +| `manage_workers` | Padrón / expediente | +| `manage_projects` | Obras | +| `manage_companies` | Empresas | +| `manage_budget` | Presupuesto | +| `manage_payroll` | Nómina / préstamos | +| `manage_documents` | Documentos | +| `manage_users` | Usuarios del tenant | +| `manage_settings` | Config tenant | +| `view_reports` | Tableros | +| `view_expenses` / `manage_expenses` | Gastos y control de costos | +| `view_warehouse` / `manage_warehouse` / `close_warehouse` | Almacén | +| `manage_cost_settings` | Umbrales presupuestales | + +`tenant_admin` tiene todos. `user` de fábrica: workers, documents, reports, view_expenses, view_warehouse. `tenant_admin` no se puede recortar por API. + +Granularidad: gastos/almacén/costos ya chequean permiso; el resto de rutas core a menudo solo `requireCoreAuth`. Evolucionar permisos de forma incremental. + +--- + +## 6. Dominio de negocio (`core`) + +### Empresas y obras + +- `companies`: `kind` `principal` | `sub`, `parent_id`, datos fiscales IMSS/RFC, `tenant_id`. +- `projects`: obra. Status `activo` | `pausado` | `concluido` | `cancelado`. Tema de gafete, logos, contrato, SIROC, `%` impuesto nómina, empresa dueña. + +### Padrón (persona maestra) + +Invariantes (`docs/padron-v2.md`): + +1. No se borra persona: `status=baja`. +2. Solo asignar/importar a proyectos **activos**. +3. Asignación repetida se **reactiva**; al finalizar se guarda `end_date`. Unique `(worker_id, project_id)`. +4. `pipeline_status` **no** es editable. Baja es manual; el resto se recalcula. +5. CURP, RFC, NSS únicos (`citext` CURP/RFC). +6. Expediente obligatorio: foto, INE, CURP, NSS/IMSS, RFC. +7. Color de gafete = código exacto de `risk_levels`; Alto/Medio/Bajo es UI. + +Campos clave worker: nombres, `hire_type`, `work_type` `N` (jornal) | `D` (destajo), `daily_wage`, `needs_badge`, `imss_status` (`sin_alta` | `alta` | `baja_imss`), empresa IMSS. + +Validación MX en `api/mx.ts`. Import Excel: template + `POST /v1/workers/import`. + +Documentos cifrados AES-GCM (`DOCS_KEY` = 64 hex). Storage key + `iv` en tablas; bytes en R2. Tipos con `validity_mode` `none` | `freshness` | `expiry`. Mismo patrón para docs de proyecto y empresa. + +### Gafetes + +Temas, jobs PDF, QR/vCard (`VCARD_BASE`). Pipeline “listo para gafete” derivado. Kanban visualiza ese pipeline. + +### Presupuesto + +Capítulos + partidas (`budget_chapters` / `budget_items`). Import/export Excel, preview. Ligado a proyecto. + +### Programa de obra + +Un programa por proyecto. Periodos semanales. Slots de concepto, montos por partida, avance. Import, wizard, recálculo vs costo. Export Excel. + +### Gastos y control de costos + +`expense_entries` + adjuntos, IVA por periodo, `tenant_cost_settings`. Control: summary / items / chapters / deviations vs presupuesto. + +### Almacén + +Materiales, almacén central y por obra, stock, movimientos, transferencias. Abrir/cerrar almacén de obra. Links material ↔ presupuesto. + +### Nómina + +Dos líneas históricas: + +- Semanas (`payroll_weeks`, sheets, lines): asistencia, assemble/pay, CSV, jornal, líneas admin, destajo (unidades, jobs, cortes). +- Periodos (`payroll_periods` / `payroll_lines`) y settings. +- Préstamos (`loans` / `loan_payments`) + PDFs de recibo. +- Sync de costo de nómina hacia control de costos (`025-rpc-payroll-cost-sync`). + +--- + +## 7. API HTTP (resumen) + +Prefijo `/v1`. CORS credenciales; origins locales 3000/3001 + `CORS_ORIGINS`. + +| Área | Rutas | +|------|--------| +| Salud | `GET /health` — core, platform, redis.iam, redis.core, storage | +| Auth | `/auth/login`, `logout`, `me`, `change-password` | +| SaaS | `/saas/tenants`, SMTP get/put/test, send-access | +| Catálogos / config | `/catalogs`, `/configuracion` | +| Empresas | CRUD + documentos | +| Proyectos | CRUD + documentos + budget nested | +| Workers | list/get/create/patch, validate, pipeline, assign, import, documents, photo | +| Gafetes | `/badge-qr`, `/projects/:id/badge-jobs`, `/badge-jobs` | +| Nómina | `/attendance`, `/payroll/*`, `/destajo/*`, `/loans` | +| Gastos | `/expenses`, `/iva/summary`, `/cost-settings` | +| Costos | `/projects/:id/cost-control/{summary,items,chapters,deviations}` | +| Almacén | `/warehouses`, stock, movements, entries, exits, transfers, open/close | +| Programa | `/projects/:id/work-program` (+ concepts, partidas, vs-cost, import, generate, export, patches de periodos) | +| IAM | `/iam/users`, `/iam/permissions`, `/iam/roles/:role/permissions`, `/me/permissions` | + +Handlers: `main.ts` + `payroll_http.ts`, `expenses_http.ts`, `warehouse_http.ts`, `cost_control_http.ts`, `work_program_http.ts`, `iam_http.ts`. + +--- + +## 8. Fronts + +### `web-panel` — escritorio + +UI tipo escritorio: layout `default.vue` (cinta, árbol, detalle, statusbar). Estado compartido `useDesktop()`. Tema claro/oscuro (`panel-theme`). Auth global; `must_change_password` → `/cambiar-password`. + +Menú: + +| Ruta | Módulo | +|------|--------| +| `/` | Inicio | +| `/proyectos` | Obras / empresas | +| `/presupuesto` | Presupuesto | +| `/programa-obra` | Programa de obra | +| `/control-presupuesto` | Control de costos | +| `/gastos` | Gastos | +| `/almacen` | Almacén | +| `/padron`, `/padron/:id` | Padrón + ficha | +| `/kanban` | Pipeline de personal | +| `/gafetes` | Impresión de gafetes | +| `/nomina` | Nómina | +| `/usuarios` | IAM del tenant | +| `/configuracion` | Config (menú perfil) | +| `/login` | Login | + +También existe `/obras.vue` en pages (legado / no está en el menú principal). + +Cliente HTTP: `composables/useApi.ts` + envelopes en `api-response.ts`. Proxy Nitro `/v1` → `127.0.0.1:8000`. + +### `web-saas` + +Rutas: `/login`, `/` (tenants), `/smtp`. Misma API. No comparte UI con el escritorio. + +--- + +## 9. Reglas para agentes (no romper) + +1. **No SQLite.** Postgres + Redis + R2. +2. **No JOIN/FK** `iam` ↔ `core` ni platform ↔ product. Snapshots desnormalizados (`uploaded_by`, etc.). +3. Schema nuevo = changeset Liquibase nuevo. Nunca editar changeset ya aplicado en ambiente compartido. +4. Negocio nuevo en `core.fn_*` + `callCoreFn`. Envelope + mensaje en español específico. +5. Rutas `core`: `...requireCoreAuth` para RLS. +6. Documentos: cifrar; no guardar plaintext en disco/R2. +7. Padrón: no DELETE de personas; pipeline derivado; unicidad CURP/RFC/NSS. +8. Coolify: solo `docker-compose.yml`. Overlay local no va a prod. +9. Secrets: `.env` gitignored. `DOCS_KEY` 64 hex. Fail-fast en production si faltan secretos. +10. Fronts: SPA + PrimeVue; proxy `/v1`; no llamar la API cross-origin en prod. +11. Tests API: `cd api && deno task check && deno task test`. Front: `npm run build` en `web-panel` / `web-saas`. +12. `web/` no es el panel. Producto = `web-panel` + `web-saas` + `api`. + +--- + +## 10. Deploy (Coolify) + +Por ambiente: 1 Postgres (2 bases), 1 Redis (2 ACL), 1 bucket R2, 4 contenedores (`migrate`, `api`, `web-panel`, `web-saas`). Red Docker `coolify` para hostnames internos UUID (no container id de 12 chars). + +Health: `GET /v1/health` debe reportar core, platform, redis.iam, redis.core y storage (`backend: "s3"` en prod). Detalle ops: `docs/coolify.md`, `db/README.md`, `db/provision/README.md`.